Compare commits

...
Author SHA1 Message Date
Nick ShirokovandClaude Opus 5 fddaca37e7 docs(meta-edit): убрать из инструкции описание реакции на ошибку
Инструкция навыка отвечает на вопрос «как вызвать правильно». Фразы вида
«опечатка в имени свойства → ошибка (правка не теряется молча)» описывают
поведение при неправильном вызове: модели, которая пишет вызов, они ничего не
дают, а сообщение об ошибке навык и так выдаст сам.

Убраны три таких места в properties-reference.md и child-operations.md.
Соседние утверждения сохранены — это настоящие ограничения: свойство можно
задать, даже если оно ещё не выставлено; допустимы имена свойств
соответствующего типа объекта; структурные свойства через Ключ=Значение не
задаются, кроме Type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:26:47 +03:00
Nick ShirokovandClaude Opus 5 50bc9f5c0a fix(meta-edit): канонизация значений свойств-перечислений
modify-property писал значение свойства объекта в XML как есть: и "обороты", и
"turnovers", и "ЧтоУгодно". Навык печатал Modified: 1 и оставлял выгрузку,
которую платформа не примет. Расхождения портов тут не было — оба вели себя
одинаково, поэтому кампания паритета это не ловила.

Значение теперь проходит через normalize_enum_value — ту же функцию, что уже
применялась к свойствам реквизитов: алиас и регистр приводятся к канону,
неизвестное значение отвергается с перечислением допустимых ДО записи файла.
Поведение сведено к meta-compile, который так работает давно.

Словарь алиасов в meta-edit обёрнут в CIDict: без этого правка сделала бы хуже
— "обороты" строчными не нашли бы ключ "Обороты" и получили бы отказ вместо
прежней тихой записи.

check-inline-drift: заведена семья normalize_enum_value (тела в обоих портах
совпадают побайтово, эталон meta-compile). Списки значений по-прежнему держит
check-enum-drift, новых ключей не заводили.

Проверка: два кейса (синоним → канон в эталоне; мусор → expectError), 685/685
на PS и 682+3 skipped на PY, гарды 4/4. Платформенно: синтетический регистр,
правка через meta-edit, загрузка в 8.3.27 и обратная выгрузка — платформа
вернула Turnovers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:16:07 +03:00
Nick ShirokovandClaude Opus 5 5baece90d6 fix(cf-edit,subsystem-edit,interface-edit): регистр значения операции
Волна закрыла ключи DSL и параметры CLI, но не значения внутри DSL: имя
операции сравнивалось точным совпадением, тогда как PS диспетчеризует через
switch, а он регистронезависим. На cf-edit "Modify-Property" PS применял, а py
писал Unknown operation; на subsystem-edit "Add-Child" py молча ничего не делал.

Имя операции теперь нормализуется в нижний регистр. skd-edit не затронут: у
него операция приходит параметром с choices, что уже закрыто ci_parse_args.

Заодно переписан кейс form-edit/lenient-key-case: он был написан в форме
operations/op, которой у навыка нет, — навык такой вход игнорирует, и кейс
проходил на обоих портах, не проверяя ничего. Теперь форма взята из
документации навыка, а в эталоне есть след эффекта. Кейсы cf-edit,
subsystem-edit и interface-edit расширены регистром значения операции.

Проверка: 683/683 (PS), 680+3 skipped (PY), гарды 4/4, 11 снэпшотов приняты
платформой, версии портов совпадают.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 20:36:48 +03:00
Nick ShirokovandClaude Opus 5 57b0f95b5f fix(skills): регистронезависимые ключи DSL в py-портах
Дорожка DSL из кампании паритета. PowerShell читает свойства объекта из
ConvertFrom-Json без учёта регистра, Python — точным совпадением, поэтому
"Name" вместо "name" в py-порте молча не находился: навык печатал [OK], а
свойство в выход не попадало. На skd-compile это давало схему без запроса.

CIDict + ci_json (эталон — meta-compile) внесены в 10 портов, читающих
пользовательский DSL: cf-edit, form-compile, form-edit, interface-edit,
meta-edit, mxl-compile, role-compile, skd-compile, subsystem-compile,
subsystem-edit. Обёрнуты точки разбора DSL, операций и JSON, приходящего
значением параметра; реестр .v8-project.json намеренно не трогаем.

skd-edit в список не входит: он принимает операцию через -Operation с
choices, что уже закрыто ci_parse_args.

Каждому навыку добавлен кейс lenient-key-case: вход с перемешанным регистром
ключей, эталон один на оба порта — раннер гоняет обе среды, поэтому кейс сам
по себе проверяет паритет.

Проверка: 683/683 (PS), 680+3 skipped (PY), гарды 4/4, версии портов совпадают.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 20:15:39 +03:00
Nick ShirokovandClaude Opus 5 d325a3d5af fix(skills): регистронезависимые параметры во всех py-портах
Дорожка CLI из кампании паритета: PowerShell не различает регистр ни в именах
параметров, ни в значениях [ValidateSet], argparse различает и в том и в
другом. Проверено на читающем навыке: cf-info -Mode BRIEF на PS отрабатывает,
на py падал.

ci_parse_args (эталон — meta-compile) вставлен в 68 py-портов, вызовы
parser.parse_args заменены. Навыков с DSL меньше трети, поэтому именно эта
правка делает паритет общим: *-info, *-validate, db-*, cfe-*, web-* тоже
принимают ввод так же, как их .ps1.

Реестр check-inline-drift дополнен списком потребителей — и сразу окупился:
поймал, что массовая замена переписала последнюю строку внутри самого
хелпера и копии стали рекурсивными.

Версии бампнуты синхронно в обоих портах всех 68 навыков.

Проверка: 673/673 (PS), 670+3 skipped (PY), гарды 4/4; паритет версий портов
проверен по заголовкам.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 20:00:31 +03:00
Nick ShirokovandClaude Opus 5 8c0fbed7d6 fix(meta-compile): регистронезависимый ввод в py-порте — паритет с PS
PowerShell не различает регистр нигде, куда попадает пользовательский ввод:
свойства объекта из ConvertFrom-Json, ключи Hashtable, -eq/-contains, имена
параметров, ValidateSet. Python различает везде, поэтому один и тот же DSL
давал разный результат на разных портах — и чаще всего молча: "CodeLength"
вместо "codeLength" в py просто не находился, навык печатал [OK], а свойство
в выход не попадало.

Пилот на meta-compile. В py-порт добавлены общие обёртки: CIDict (поиск без
учёта регистра, ключи хранятся как есть — часть из них имена объектов и
попадает в XML), ci_json (рекурсивно на разобранный DSL), ci_parse_args (имена
параметров и значения choices). Ими же обёрнуты словари синонимов видов,
алиасов enum и типов.

Попутно исправлен дефект самого канона: PS принимал "type":"catalog", но
дальше использовал значение как есть — в имени тега и в регистрации в
Configuration.xml, то есть отдавал <catalog>, которую платформа не примет.
Теперь вид приводится к канону списка в обоих портах.

check-inline-drift: extractPy научен доставать class (иначе тело CIDict
невидимо), заведены три семьи с ps1: null — в PS1 этих обёрток быть не должно.

Проверка: кейс lenient-key-case зелёный на обоих портах; полный регресс
673/673 (PS) и 670+3 skipped (PY); гарды 4/4; sweep по 400 справочникам трёх
конфигураций (декомпиляция → компиляция обоими портами) — 0 расхождений.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 19:48:24 +03:00
Nick ShirokovandClaude Opus 5 4622234b52 feat(meta-compile): пары субконто бухрегистра из плана счетов
Компилятор эмитил ExtDimensionN/ExtDimensionTypeN только по ключам DSL,
поэтому регистр, созданный по неполному описанию, не совпадал с тем, что
материализует платформа: пары дописывались лишь при загрузке.

Теперь их число выводится из MaxExtDimensionCount плана счетов, на который
ссылается регистр, — файл читается из выгрузки, как версия формата из
Configuration.xml. ExtDimensionN получает LinkByType на Account с LinkItem
по номеру. Выведенный хвост дополняет DSL, а не заменяет: ключи из описания
остаются, недостающее добавляется.

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

Проверка: матрица из трёх случаев (план с 3 субконто, план с 0, план
отсутствует) на обоих портах; синтетика загружена в 1С и выгружена обратно —
состав и порядок совпали; корпусный роундтрип 739 регистров без расхождений.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 18:42:00 +03:00
Nick ShirokovandClaude Opus 5 16199deb92 fix(meta-compile,meta-validate): канон стандартных реквизитов регистров
Справочник порядка StandardAttributes был неполон у трёх видов регистров, а
неизвестные имена эмитятся перед фикс-списком — платформа их вкрапляет, и
роундтрип переставал быть побайтовым.

Порядок снят с выгрузки платформы, присутствие условных реквизитов теперь
выводится из свойств объекта (periodAdjustmentLength, correspondence,
registerType), а не из наличия ключа в DSL: иначе регистр, создаваемый с нуля,
терял реквизит, который платформа обязана материализовать. Наличие ключа
осталось дополняющим условием, чтобы роундтрип не зависел от точности вывода.

- регистр расчёта: 11 реквизитов, состав безусловен (проверено матрицей
  actionPeriod × basePeriod × периодичность на 8.3.27)
- регистр бухгалтерии: PeriodAdjustment при длине периода корректировки > 0,
  RecordType при correspondence=false
- регистр накопления: RecordType только у регистра остатков

meta-validate: снято ложное предупреждение об «unexpected» RecordType у
бухрегистра, условные реквизиты исключены из проверки missing.

Проверка: 739 регистров по шести конфигурациям — 0 расхождений; снэпшоты
пересняты и верифицированы загрузкой в 1С.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 18:06:34 +03:00
Nick ShirokovandClaude Opus 5 707a5c293f fix(cfe-patch-method): паритет портов по прощающему вводу + карта в реестр
Проверка «работает ли гард во все стороны» вскрыла, что он проверяет только
заявленные карты. Попытка автообнаружения незаявленных дала 112 срабатываний,
из них большинство — ложные (накопители массивов, чей блок разбирается неверно),
поэтому сама проверка в гард не попала. Но она нашла реальное:

у cfe-patch-method карта типов не была в реестре, и порты по ней разошлись —
PS1 принимал и Catalog.X, и Catalogs.X (32 записи), PY только Catalog.X (16).
Расхождение доставшееся, не из этой ветки. PY дополнен формами множественного
числа: ввод, работавший в одном порте, теперь работает в обоих.

В реестр карт типов добавлены cfe-patch-method (TYPE_DIR_MAP, DIR_TO_TYPE),
cf-edit.RU_TYPE_MAP и cfe-borrow.SYNONYM_MAP — 28 проверяемых карт стало 36.
Для частичных карт добавлен флаг partial (полнота не требуется, каталоги
сверяются), для прощающих — keyMayBeDir (ключом принимается имя каталога).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 15:27:44 +03:00
Nick ShirokovandClaude Opus 5 04d78fe816 chore(*-info): бамп версий после переименования хелпера
Шесть *-info изменены переименованием is_external_root ->
_sg_is_external_root без бампа версии. Поднято в обоих портах синхронно, как
требует конвенция, хотя менялся только .py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:57:34 +03:00
Nick ShirokovandClaude Opus 5 63e711905c fix(tests): экстракторы гарда пропускали копии — ложное «OK»
Разбор остатка по support-guard показал, что расхождения портов у *-info нет:
они читают тем же хелпером состояние поддержки для вывода, и фича есть в обоих
портах. Пропускал копии сам гард.

Два дефекта извлечения, оба давали ложное «OK», а не ложную тревогу:

1. PY: вложенные определения. Пять *-info объявляют is_external_root внутри
   другой функции; экстрактор искал только `^def` и перескакивал через тело
   внешней функции, так что вложенные для гарда не существовали.
2. PS1: однострочные функции. subsystem-info.ps1:18 — `function Out(...) { ... }`
   в одну строку; поиск закрывающей `}` на отдельной строке делал «телом» Out всё
   до следующей одиночной скобки, проглатывая следующую функцию.

После починки гард увидел 9 копий, которых не видел, — все совпали с эталонами.
Побочно вскрылось, что Report-OK в interface/subsystem/xdto-validate тоже был
невидим и потому не попал в прошлое сведение Report-*; теперь сведён.

Осталось расхождение имён: одно тело называлось _sg_is_external_root (17),
is_external_root (5 *-info) и _meta_is_external_root (meta-info). В PS1 имя было
единым изначально; PY сведён к _sg_is_external_root.

Реестр: 24 семьи, 319 копий (было 310), долг ноль.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:42:46 +03:00
Nick ShirokovandClaude Opus 5 78ac3a8c36 fix(xdto-compile,xdto-edit): заменить урезанный support-guard на общую реализацию
Своя версия решала по правилу «файл Ext/ParentConfigurations.bin существует →
запретить». Общая разбирает заголовок bin и решает по конкретному объекту; в
частности у неё есть ветка `if k == 0: return` — если на поддержке нет ни одного
объекта, запрещать нечего.

Эксперимент на копии каталога конфигурации ERP (заголовок bin = {6,0,0,0,1,0},
K=0): meta-compile создавал объект, xdto-compile на том же каталоге отказывал.
То есть XDTO-пакет нельзя было добавить туда, где справочник добавляется
свободно. У acc_8.3.24 и ut_8.3.27 заголовок {6,1,1,...} (G=1, вся конфигурация
read-only) — там обе реализации отказывали одинаково.

Своя версия строго более ограничительна, поэтому это не дыра, а ложные отказы
плюс неинформативное сообщение. После правки ERP разрешает, acc по-прежнему
отказывает — и уже с диагнозом и шагами через support-edit.

Долг реестра inline-реализаций обнулился: 24 семьи, 310 копий, вариантов без
обоснования не осталось.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:23:37 +03:00
Nick ShirokovandClaude Opus 5 2eb25be7c5 test(skills): гард согласованности карт типов метаданных со спецификацией
Прямой ответ на вопрос ишью #60. Гард inline-реализаций держал функции, но не
словари — а именно там расхождение и накапливалось молча: тип Bot существовал в
таблице спецификации и в трёх навыках, в остальных двенадцати его не было, и
заметить это было нечем.

Эталон — таблица «Порядок типов в ChildObjects» из docs/1c-configuration-spec.md
(45 типов: имя, каталог, позиция). Берём документацию, а не отдельный JSON: тогда
спека и код не расходятся молча, что и было целью ишью.

Модель двухуровневая, как и предлагалось в обсуждении: общее ядро (имя, каталог,
порядок) обязано совпадать у всех, а навык объявляет своё подмножество —
исключение с ПРИЧИНОЙ. Проверка отличает намеренное ограничение от забытого типа.
Сейчас исключение ровно одно: Language в cfe-diff, где записи в карте были бы
недостижимы.

Вокабуляры навыков (TYPE_ALIASES, TYPE_NORM_MAP, CONTENT_TYPE_MAP, TYPE_PLURAL_MAP)
проверяются слабее: полнота не требуется, но каждое каноническое имя обязано
существовать в таблице — это ловит опечатки. Пустое извлечение карты считается
ошибкой разбора, иначе непонятый формат прошёл бы вхолостую.

Проверено негативом: убранный тип, подменённый каталог и переставленный порядок
дают ERROR и exit 1.

cfe-borrow/SKILL.md: убрано обещание про количество поддерживаемых типов — оно уже
протухло (было 44) и является обязательством, которое навык не проверяет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:57:50 +03:00
Nick ShirokovandClaude Opus 5 34ac9a18d0 fix(interface-edit,role-compile): дополнить русские имена типов
interface-edit знал CalculationRegisters и ChartsOfCalculationTypes только
по-английски: РегистрРасчёта/РегистрРасчета и ПланВидовРасчёта/ПланВидовРасчета
не принимались ни в каком написании. Добавлены также Роль, ОбщийМакет,
ЭлементСтиля, ОбщийРеквизит, ГруппаКоманд — в единственном и множественном числе.

role-compile принимал только формы без ё (Отчет, РегистрРасчета,
ПланВидовРасчета), тогда как meta-compile, form-compile и subsystem-навыки
принимают обе. Добавлены варианты с ё.

Правка аддитивная: ввод, который работал, продолжает работать.

cfe-borrow/SKILL.md: «все 44 типа» -> 45 (таблица ChildObjects включает Bot и
Language, оба подтверждены платформенной пробой).

Кейс role-compile/type-aliases-yo проверен негативным прогоном на своём порте.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:45:23 +03:00
Nick ShirokovandClaude Opus 5 a0a91a4e40 fix(cfe-diff,cfe-borrow,cfe-validate,subsystem-*): вернуть недостающие типы
Платформенная проба (свежий стенд 8.3.24, загрузка + обратная выгрузка) сняла
вопросы, которые ревизия #60 оставила на догадках:

  Bot в ChildObjects конфигурации      — принят, пережил раундтрип
  Bot.X в <Content> подсистемы          — принят, пережил раундтрип
  Bot в ChildObjects расширения         — принят, пережил раундтрип
  второй Language заимствован в расш.   — принят, пережил раундтрип

Порядок, выданный платформой (Language, Subsystem, Bot), совпал с таблицей
docs/1c-configuration-spec.md, где Bot стоит под № 11.

Правки:
- cfe-diff: +Style, +XDTOPackage, +WebService, +HTTPService, +WSReference, +Bot
  (38 -> 44). Карта используется в трёх местах — обзор Mode A, поиск модулей и
  проверка переноса Mode B, — поэтому объект неизвестного типа выпадал из всех.
- cfe-borrow: +Bot, +Language (43 -> 45), Bot в TYPE_ORDER;
- cfe-validate: +Bot (44 -> 45);
- subsystem-compile, subsystem-edit: +Bot в CONTENT_TYPE_MAP.

Language в cfe-diff НЕ добавлен: навык намеренно пропускает языки при сборе
объектов (cfe-diff.py:530), запись в карте была бы недостижима.

Прежняя оценка «отсутствие Language в cfe-borrow вероятно законно» опровергнута:
в расширении из корпуса язык помечен ObjectBelonging=Adopted, то есть заимствован,
и второй язык заимствуется штатно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:31:11 +03:00
Nick ShirokovandClaude Opus 5 ff01102b9b docs(tests): зафиксировать природу расхождения оставшихся семей
Храповик без пояснения читается как «когда-нибудь свести». Разбор показал, что
из четырёх оставшихся семей одна вообще не является общей: import_fragment имеет
разные сигнатуры и разные наборы xmlns по навыкам — одно имя на разные задачи,
сводить его нельзя. Остальные три сводятся, но каждое сведение меняет вывод и
требует решения, а не аккуратности.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 12:30:54 +03:00
Nick ShirokovandClaude Opus 5 01749ad6ee refactor(skills): свести write_xml_file и write_utf8_bom к общему эталону
У write_xml_file в docstring прямо написано «Держать копии одинаковыми —
сознательно: разошедшиеся копии сводят на нет весь смысл», и при этом копий было
три варианта. Различие оказалось не в логике нормализации, а в имени
вспомогательной функции записи байт: write_utf8_bom (11 навыков),
write_text_with_bom (form-add, help-add, template-add), save_text_bom
(cfe-borrow) — тела всех трёх побайтово эквивалентны.

Имя сведено к write_utf8_bom, тело помощника — к общему эталону (различались
только имена переменных). PS1-порт Write-XmlFile расхождений не имел.

Обе семьи переведены из храповика в реестр с эталоном: 24 семьи, 309 копий,
разъехавшихся осталось 4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 12:29:43 +03:00
Nick ShirokovandClaude Opus 5 f2216745e6 refactor(validate): свести Report-* к общему эталону вывода
interface-validate, subsystem-validate и xdto-validate отличались от остальных
шести валидаторов только стилем: инлайн-сигнатура вместо param-блока и
однострочный if. Приведены к эталону cf-validate.

form-validate и mxl-validate оставлены отдельным вариантом с обоснованием:
они пишут через Write-Host, а не через буферизованный Out-Line. Буферизация
нужна для -OutFile (тот же отчёт в файл), которого у этих двух навыков нет;
для модели-потребителя stdout одинаков — на успешном прогоне обе ветки дают
одну строку.

Семьи Report-OK/Error/Warn переведены из храповика в реестр с эталоном.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 21:23:58 +03:00
Nick ShirokovandClaude Opus 5 fadda6a287 refactor(meta-validate,web-publish): свести format_rank и db-обвязку к эталонам
format_rank в meta-validate отличался только именем параметра, _version_key в
web-publish инлайнил каталог версии вместо общего _version_dir (и знал только
Windows-раскладку .../bin/exe). Обе копии приведены к эталону db-create/
meta-compile; для Windows-навыка web-publish поведение не меняется.

В реестр добавлены семьи _version_dir и _find_project_v8path.

Экстрактор PS1 в гарде и сканере научен видеть вложенные определения: db-load-git
объявляет Find-ProjectV8Path внутри if, и поиск по `function` с нулевой позиции
считал функцию отсутствующей. Конец тела теперь ищется по закрывающей скобке на
отступе самого слова function.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 21:13:50 +03:00
Nick ShirokovandClaude Opus 5 974ca3aac9 fix(form,skd,init,meta-edit): развести экранирование атрибута и текста
esc_xml был один на две задачи, и в атрибутном контексте применялся текстовый
вариант без &quot;. form-compile с choiceParameters, имя которого содержит
кавычку, выдавал <app:item name="Отбор.Наименование "кавычка"">: lxml падает с
attributes construct error, то есть файл невалиден как XML. Затронуто 9 мест в
form-compile и 3 в skd-compile, одинаково в обоих портах.

Раундтрип через базу показал границу: в ТЕКСТЕ элемента платформа экранирует
только & < > (кавычка и апостроф возвращаются сырыми байт-в-байт), а в ЗНАЧЕНИИ
АТРИБУТА пишет &quot; — внутри "..." литеральная кавычка невалидна. Поэтому две
функции с говорящими именами, а не одна с флагом:
  esc_xml      — значение атрибута: & < > "
  esc_xml_text — текст элемента:    & < >

Заодно закрыто расхождение портов в init-навыках: PY экранировал текст с &quot;,
PS1 — через SecurityElement::Escape (ещё и &apos;), а epf-init/erf-init в PS1 не
экранировали вовсе, из-за чего амперсанд в синониме давал невалидный XML.

Кейсы: form-compile/attr-value-escaping, skd-compile/additional-properties-escaping,
cf-init/synonym-escaping — все три проверены негативным прогоном на обоих портах
и верификацией снэпшотов на платформе.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:53:12 +03:00
Nick ShirokovandClaude Opus 5 3c50d3caa0 fix(form,meta,skd): срезать любой сгенерированный префикс dNpM:, не только d5p1
Имя префикса для .../8.1/data/enterprise/current-config платформа выдаёт по
порядку объявления в файле: в корпусе acc_8.3.27 на этом URI встречаются d4p1,
d5p1 и d6p1. Литеральное d5p1: покрывало лишь часть случаев — тип, скопированный
из Predefined.xml, приходит с d4p1: и даёт ту же ошибку «Неизвестное имя типа».

Обобщено до ^d\d+p\d+: — так же, как это уже делает form-info. Условие «только у
ссылочных типов, с точкой» сохранено: все 11 спец-типов чужих пространств имён
(d5p1:Chart, mxl:SpreadsheetDocument, pl:Planner и др.) точки не имеют.

Регресс-кейс расширен на d4p1 и d6p1: все шесть форм ввода дают один
cfg:CatalogRef.Склады.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:04:06 +03:00
Nick ShirokovandClaude Opus 5 826ba3de71 fix(form,meta,skd): resolve_type_str — одно тело + срезание префикса типа
Шесть копий resolve_type_str разошлись на 6 вариантов в PY и 4 в PS1, а вместе
с формой разошлось и поведение: префикс ссылочного типа срезал только
form-compile.

Проверка на платформе (стенд 8.3.24.1691, cf-init → meta-compile → db-load-xml)
показала, что это не косметика. Префикс ломает поиск в словаре синонимов, из-за
чего русское имя типа остаётся непереведённым:
  СправочникСсылка.Склады      → cfg:CatalogRef.Склады        (верно)
  cfg:СправочникСсылка.Склады  → cfg:СправочникСсылка.Склады  (неверно)
Платформа отвечает «Неизвестное имя типа - СправочникСсылка.Склады» и
отклоняет загрузку. Именно с префиксом тип и написан в выгрузке, откуда
пользователь его копирует.

cfg: снимается всегда, d5p1: — только у ссылочных типов (с точкой): сам по себе
префикс неоднозначен, в формах d5p1:Chart, d5p1:TextDocument и ещё три типа
адресуют свои пространства имён, и там он часть канонического значения. Первая
версия правки срезала его безусловно и уронила 4 кейса form-compile.

Тело общее для всех шести навыков; словари синонимов остаются локальными, навык
объявляет на свой словарь алиас TYPE_SYNONYMS / $script:typeSynonyms.

Регресс-кейс meta-compile/type-prefix-lenient проверяет все три формы ввода.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 19:44:39 +03:00
Nick ShirokovandClaude Opus 5 d99d63288e refactor(skills): detect_format_version — один вариант на всех потребителей
Семья была разбита на «знает про автономную EPF/ERF» (form-*, mxl-compile,
template-add, help-add) и «не знает» (cfe-borrow, interface-edit, meta-compile,
role-compile, subsystem-compile, xdto-compile), плюс редакторски разошедшаяся
PS1-копия help-add.

Расширенный вариант корректен и для конфигурационных навыков: ветка срабатывает
только если <каталог>.xml — корень ExternalDataProcessor/ExternalReport, чего в
дереве конфигурации не бывает. Переключатель не нужен — раскопирован как есть.

Семья в реестре схлопнута до одного варианта: 11 копий, оба порта.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 18:18:52 +03:00
Nick ShirokovandClaude Opus 5 1586e909a9 fix(meta-validate,meta-edit): ReuseSessions — вернуть значение Use
Оба навыка бракуют корректное значение: allowlist содержал только
[DontUse, AutoUse], тогда как meta-compile знает три значения.

Прав meta-compile: docs/meta-dsl-spec.md перечисляет DontUse / Use / AutoUse,
в выгрузке прикладной конфигурации встречаются все три (DontUse ×17,
AutoUse ×2, Use ×1).

Расхождение ловил check-enum-drift.mjs, но гард не был подключён ни к runner-у,
ни к README, поэтому не запускался. Теперь он входит в check-all.mjs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 18:07:46 +03:00
Nick ShirokovandClaude Opus 5 c8144a01e5 test(skills): реестр и гард общих inline-реализаций
Навыки автономны, общие утилиты копируются в каждый .ps1/.py, но нигде не было
зафиксировано, какая копия эталонная и какие расхождения законны. Ревизия по #60
показала два класса семей: одни держатся побайтово (support-guard — 16 копий,
db-обвязка — 12), другие разъехались без эталона (resolve_type_str — 6 копий и
6 вариантов, get_ml_text — 7 и 7).

check-inline-drift.mjs держит реестр семей внутри себя: вариант → эталон → копии.
Копия обязана совпадать с эталоном своего варианта; отклоняющийся вариант обязан
иметь обоснование, иначе печатается как долг. Часть расхождений законна (esc_xml
без &quot; в form-* ради раундтрипа), поэтому модель хранит варианты, а не одно
эталонное тело. Разъехавшиеся целиком семьи стоят на храповике maxVariants.

Извлечение тел: PS1 — до строки ровно `}` (балансировка скобок даёт ложные
18 вариантов из 18 копий Assert-EditAllowed); PY — с обязательным снятием
docstring-ов (иначе одинаковый код с разным описанием читается как расхождение).

check-all.mjs — единая точка входа: check-enum-drift и check-uuid-invariant были
рабочими, но не упоминались в README и никем не запускались.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 17:22:33 +03:00
Nick ShirokovandClaude Opus 5 fc2d460b57 docs(web-info,web-publish): убрать детали реализации из инструкций
В «Как читать вывод» протекли внутренности фильтра журнала (коды сообщений
Apache, что именно отсеивается и почему) и обоснование правки. Модели,
использующей навык, нужны сигналы и действие по ним, а не устройство.
В web-publish по той же причине убрано имя атрибута дескриптора.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 15:48:32 +03:00
Nick ShirokovandClaude Opus 5 0d4933f135 feat(web-info): проверка TCP-порта Apache
Статус строился только по наличию процесса httpd: при живом процессе и
не поднявшемся сервере (занятый порт, битый конфиг) навык рапортовал
«Запущен». Теперь порт из Listen проверяется TCP-коннектом на 127.0.0.1
с таймаутом 1 с — «(слушается)» / «(не отвечает)».

Сами публикации намеренно не опрашиваются: запрос к /{AppName} поднимает
сеанс веб-клиента 1С и занимает лицензию, а публикаций может быть много.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 15:46:22 +03:00
Nick ShirokovandClaude Opus 5 b8cbb0111a feat(web-info): в секции ошибок только error и выше + путь к журналу
Печатались последние 5 строк журнала как есть, поэтому секция «Последние
ошибки» показывала notice старта, а на Windows ещё и штатный шум
winnt_accept — содержательные записи вытеснялись. Теперь фильтр по уровню
error/crit/alert/emerg; AH02538 отсеивается отдельно как след нашего же
рестарта Apache при публикации.

Путь к журналу печатается всегда — за отсеянными уровнями и полным
контекстом читается файл.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 15:37:24 +03:00
Nick ShirokovandClaude Opus 5 88535a759b fix(web-info): имя журнала ошибок из httpd.conf + описание вывода вместо примера
Секция «Последние ошибки» всегда печатала «(нет файла)»: путь был захардкожен
как logs/error.log, а сборка Apache пишет logs/error_log. Имя берём из директивы
ErrorLog, с фолбэком на оба общепринятых имени.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 15:28:20 +03:00
Nick ShirokovandClaude Opus 5 fbf4d4f986 fix(web-info): детект OData по <standardOdata enable> + метка сервисов расширений
Тег [OData] искался по атрибуту enableStandardOdata на <point> — форма до
8.3.9, которую web-publish не пишет с перехода на <standardOdata enable="true"/>,
поэтому в сводке не появлялся никогда. Старая форма оставлена как fallback
для дескрипторов не нашего происхождения.

Плюс суффикс +ext на WS/HTTP, когда в дескрипторе publishExtensionsByDefault.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 15:20:20 +03:00
Nick ShirokovandClaude Opus 5 1eeea204c3 feat(web-publish): публикация сервисов расширений в default.vrd
publishExtensionsByDefault="true" на <ws> и <httpServices>: сервисы,
поставляемые расширениями конфигурации, публикуются вместе с сервисами
самой конфигурации. Без атрибута HTTP-сервис расширения отдаёт 404,
web-сервис — 500 «Сервис не найден».

Проверено E2E на 8.3.24 (A/B на одном дескрипторе, оба порта).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 15:19:08 +03:00
Nick ShirokovandClaude Opus 5 27e78859e8 fix(tests): отпечаток фикстуры рядом с каталогом, а не внутри
Файл .fixture-stamp лежал внутри каталога фикстуры, а фикстура копируется в
рабочий каталог кейса целиком — и отпечаток попал в 11 снэпшотов. Перенесён в
<cache>/<setup>.stamp, из снэпшотов удалён.

663/663 ps1, 660/663 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 13:53:25 +03:00
Nick ShirokovandClaude Opus 5 aa32fb84dc test: снята мёртвая нормализация регистра encoding в снэпшотах
Третья маска подряд, на которую не опирался ни один кейс: py-набор без неё
зелёный целиком (660/663). Порты пишут XML-декларацию одинаково.

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

663/663 ps1, 660/663 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 13:40:53 +03:00
Nick ShirokovandClaude Opus 5 4c84d840df fix(tests): инвалидация кэша фикстур empty-config по содержимому cf-init
Каталог фикстуры кэшировался навсегда: правка cf-init до него не доходила,
снэпшоты записывались со старой фикстурой, и расхождение всплывало только на
машине с пустым кэшем. Поймано прогоном на маке — там фикстура собралась заново
и принесла TextToSpeech, которого нет в снэпшотах, записанных на Windows.

Теперь рядом с фикстурой лежит отпечаток: аргументы плюс хэши обоих портов
cf-init. Не совпал — каталог пересобирается.

Снэпшоты кейсов 2.21 перезаписаны со свежей фикстурой (дрейф — только
TextToSpeech). У mxl-compile снэпшот кейса снят через noSnapshot: у навыка
normalizeUuids=false, поэтому UUID фикстуры дрожали бы при каждой пересборке,
а предмет кейса проверяет fileContains.

663/663 ps1, 660/663 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 12:53:38 +03:00
Nick ShirokovandClaude Opus 5 57c8cb6d23 feat(cfe-init): Caption и ShortCaption в корне расширения на формате 2.21
Свойства корня, добавленные форматом 2.21, между Version и DefaultLanguage.
В cf-init они уже были, в cfe-init — нет. Нашёл раундтрип расширения через базу
8.5 по сценарию из issue #57: загрузка расширения и обратная выгрузка давали
диff ровно на этих двух элементах.

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

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

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

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

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

663/663 ps1, 660/663 py.

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

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

663/663 ps1, 660/663 py.

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

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

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

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

648/648 ps1, 645/648 py.

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

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

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

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

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

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

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

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

648/648 ps1, 645/648 py.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:59:30 +03:00
Nick ShirokovandClaude Opus 5 8925bd5875 fix(cfe-borrow): ChildObjects и InternalInfo у контейнерных типов (#56)
Заимствованные DataProcessor, Report, DocumentJournal, HTTPService,
WebService, Subsystem и др. выводились без <ChildObjects/> — платформа
отвергала расширение при загрузке исходников («ожидаемое ChildObjects»).
Признак теперь берётся у объекта-источника, список типов остаётся
страховкой и дополнен недостающими.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Что сделано:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:15:38 +03:00
Nick ShirokovandClaude Opus 4.8 5a9b111dc0 test(meta-edit): исправить формат кейсов на документированный JSON DSL
9 кейсов задавали правки через `{operations:[{op,...}]}` — такого формата
meta-edit не понимает (ключ не в add/remove/modify), поэтому операции молча
пропускались (Warn «Unknown operation»), и снэпшоты фиксировали лишь исходный
скомпилированный объект. Тесты фактически ничего не проверяли.

Переписаны на формат из json-dsl.md: add/remove/modify → attributes/
enumValues/tabularSections/properties (shorthand-строки). Снэпшоты
переснятые — теперь реально содержат эффекты (ИНН добавлен, Устаревший
удалён, Телефон→НомерТелефона, CodeLength=11, ТЧ с реквизитами, Цена в ТЧ).
Регресс 11/11 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:37:46 +03:00
Nick ShirokovandClaude Opus 4.8 d007da5eb8 feat(meta-edit,meta-validate): типозависимый отказ на зарезервированные имена реквизитов
Имя собственного реквизита, совпадающее со стандартным (англ. ИЛИ рус.,
регистронезависимо), платформа не примет — теперь жёсткий отказ вместо
предупреждения (как в meta-compile).

meta-edit: reservedByContext (catalog/document) в Build-AttributeFragment;
прочие контексты — прежнее предупреждение, реквизиты ТЧ не проверяются.
meta-validate Check 7b: было плоско и только по англ. именам с Warn — стало
типозависимо через standardAttributesByType, EN+RU, Report-Error.

Проверка типозависима: «Номер» — легальный реквизит справочника (стандартный
только у документа). Негативные кейсы error-reserved-attr (meta-edit +
meta-validate с фикстурой). Регресс 11/11 и 13/13 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:30:34 +03:00
Nick ShirokovandClaude Opus 4.8 c2703f043f test(meta-edit,meta-validate): пересъём устаревших снэпшотов
Снэпшоты meta-edit/meta-validate строятся preRun-прогоном meta-compile и
отстали: содержали безусловный <StandardAttributes> (до условной эмиссии
df5c1ee1) — теперь блок опускается для некастомизированных объектов. Заодно
подхватился object-level CreateOnInput DontUse→Use. Регресс 10/10 и 12/12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:23:16 +03:00
Nick ShirokovandClaude Opus 4.8 b4d48ca656 feat(meta-compile): отклонять имена реквизитов, совпадающие со стандартными (v1.20)
Раньше совпадение имени собственного реквизита со стандартным (Код,
Наименование, Владелец, Родитель, Ссылка, Предопределённый, …) давало лишь
предупреждение — теперь жёсткий отказ (платформа такое имя не примет).

Проверка типозависима (reservedByContext): у справочника один набор
стандартных имён, у документа — другой. Напр. «Номер» — легальный реквизит
справочника, отклоняется только у документа. Прочие контексты сохраняют
мягкое предупреждение по плоскому списку; реквизиты ТЧ не проверяются.

Кейс catalog-object-props: «Код»→«КодНастройки» (было ложное имя). Новый
негативный кейс catalog-attr-reserved-name. spec §4. Регресс 41/41 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:07:55 +03:00
Nick ShirokovandClaude Opus 4.8 1565ae23ab feat(meta-compile): прощающий ввод ссылок на формы по умолчанию (v1.19)
Default*Form/Auxiliary*Form нормализуются как fillValue-ссылки: русский
корень (Справочник→Catalog), сегмент Форма→Form, и короткая запись без него
(Справочник.X.ФормаЭлемента ≡ Справочник.X.Форма.ФормаЭлемента ≡ канон).
Канон англ. — идемпотентно (снэпшоты не меняются). Декомпилятор эмитит
канон, роундтрип не затронут.

Кейс catalog-object-props переведён на русскую форму (лочит нормализацию),
spec §7.1. Регресс 40/40 ps1+py, ps1↔py идентичны.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:00:59 +03:00
Nick ShirokovandClaude Opus 4.8 ef20f6ece8 feat(meta-compile,meta-decompile): батч object-свойств каталога (v1.18/v0.13)
Object-уровневые свойства каталога, которые компилятор хардкодил, стали
DSL-ключами omit-on-default (зеркало батча реквизитов). Class-2 фикс:
CreateOnInput дефолт DontUse→Use (модальное значение платформы, 1163).

Новые скалярные ключи: useStandardCommands, editType, includeHelpInContents,
choiceHistoryOnInput, predefinedDataUpdate, searchStringModeOnInputByString,
createOnInput. Object comment теперь пробрасывается. 10 ключей форм по
умолчанию (default*Form/auxiliary*Form) — ссылки verbatim. Декомпилятор
эмитит все эти ключи (+ уже поддержанные defaultPresentation/quickChoice/
choiceMode/fullTextSearch). Хелперы Get-BoolProp (presence-aware) и
Emit-FormRef; +4 enum в validEnumValues.

Все root-уровневые object-property диффы = 0 на выборке (rt-op, baseline
~5.6k), регресс 40/40 ps1+py (12 снэпшотов CreateOnInput DontUse→Use).
spec §7.1, кейс catalog-object-props.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:57:30 +03:00
Nick ShirokovandClaude Opus 4.8 f11fe6808d docs(meta-dsl-spec): убрать строку версии (остальные *-dsl-spec её не ведут)
Версию вёл только meta-dsl-spec.md; form/mxl/role/skd-dsl-spec — без неё.
«Версия формата: 2.17» в epf/erf/role-спеках — версия XML-формата платформы,
другое понятие. Приведено к общей конвенции DSL-спек.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 17:53:55 +03:00
Nick ShirokovandClaude Opus 4.8 b93fd0807f feat(meta-compile,meta-decompile): значение заполнения реквизита fillValue (v1.27/v0.12)
Пара FillFromFillingValue+FillValue — единый блок «заполнения» (у top-level
реквизитов всегда, у ТЧ нет). Класс-2 фикс: boolean дефолт пустого FillValue
false→nil (модальная форма платформы, 3734 мисматча). Дефолт по типу:
String→typed-empty, Number→0, всё прочее→nil.

DSL-ключ fillValue (интерпретация по типу реквизита): bool/число/строка/дата-
литерал + null→nil-override + DTR-путь (полный рус/англ, GUID.GUID, короткая
запись по типу: EmptyRef / имя-значения перечисления / предопределённое).
Ref-резолвер портирован из form-compile (+ПустаяСсылка/Истина/Ложь, гард
составного типа). Декомпилятор: захват FillValue с omit-при-дефолте.

remaining=0 на выборке категории Attribute>FillValue (baseline-impact 7222),
регресс 39/39 ps1+py. spec v2.7 §4.2, кейс catalog-attr-fillvalue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 17:46:31 +03:00
Nick ShirokovandClaude Opus 4.8 d8146e1f1b feat(meta-compile,meta-decompile): команды объекта (Command + CommandModule.bsl) (v1.26/v0.11)
Новая возможность (не только раундтрип): meta-compile теперь умеет добавлять команды объекту
(раньше не умел вовсе). Корпус: 243 команды в 133 справочниках.

DSL `commands` (map имя→объект ИЛИ array): synonym/tooltip (ML, авто-синоним), comment, group,
commandParameterType (тип), parameterUseMode (Single), modifiesData (false), representation (Auto),
picture/shortcut, onMainServerUnavalableBehavior (Auto). Все omit-on-default.

- meta-compile (дуал-порт): Emit-Command → <Command>-блок в ChildObjects (после ТЧ) + генерация
  Commands/<Имя>/Ext/CommandModule.bsl с заготовкой обработчика ОбработкаКоманды.
- meta-decompile: захват команд из ChildObjects (тела модулей — вне скоупа, как ObjectModule).
- spec §7.1.3; тест-кейс catalog-command.

Валидация: PS==PY (XML+модуль); Command-категория 0; −6244 (53436→47192); регресс 38/38; 1С-cert
зелёный (справочник с командой+параметр-типом+модулем грузится в платформу).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 16:58:03 +03:00
Nick ShirokovandClaude Opus 4.8 1701bdac96 feat(meta-compile,meta-decompile): презентации объекта (Object/List/Extended*/Explanation, ML) (v1.25/v0.10)
Раундтрип-находка: презентации мультиязычны и непусты у многих справочников (корпус: ObjectPresentation
1316/803-multi, ListPresentation 208, Explanation 101, Extended* 69/75), компилятор писал пусто.

DSL-ключи (ML, omit-on-default): objectPresentation, extendedObjectPresentation, listPresentation,
extendedListPresentation, explanation. Эмиссия через Emit-MLText во всех 8 местах (типы, где эти свойства
есть — безопасно: без ключа → self-close = прежнее поведение). Декомпилятор захватывает через Get-MLValue.

spec §7.1; тест-кейс catalog-attr-ml расширен.
Валидация: PS==PY; ObjectPresentation/ListPresentation impact 0; −17135 (70571→53436, вкл. добитый
пробельный ToolTip-остаток нормализацией); регресс 37/37; 1С-cert зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 16:37:39 +03:00
Nick ShirokovandClaude Opus 4.8 40cdd57619 feat(meta-compile,meta-decompile): ML-синоним + ToolTip стандартных реквизитов (v1.24/v0.9)
Раундтрип-находка: у стандартных реквизитов синоним мультиязычен (корпус: 340 multi + 206 ru),
ToolTip непустой у 722 — компилятор писал синоним ru-only и <xr:ToolTip/> пусто, декомпилятор брал
Get-MLru (ru). Расширение существующего механизма standardAttributes.

- meta-compile (дуал-порт): Emit-StandardAttribute — синоним и ToolTip через Emit-MLText (строка|{ru,en});
  DSL standardAttributes.X.tooltip + synonym без стрингификации. Заодно Emit-MLItems → Esc-XmlText
  (кавычки в тексте элемента raw, не только у predefined).
- meta-decompile: синоним/tooltip станд. реквизита через Get-MLValue; Get-MLValue → $null при пустом
  ru-содержимом (пустой ML-item ≡ отсутствие значения, не tooltip:"").
- spec §7.1.1 (synonym/tooltip — ML); тест-кейс catalog-standard-attributes расширен.

Валидация: PS==PY; ToolFip станд.реквизита 5903→404 (остаток — косметика пустой-item≡self-close);
−9049 (79620→70571); регресс 37/37; 1С-cert зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:57:36 +03:00
Nick ShirokovandClaude Opus 4.8 03de2dc86d feat(meta-compile,meta-decompile): объектная форма табличной части (синоним/подсказка/комментарий ТЧ) (v1.23/v0.8)
Раундтрип-находка: синоним ТЧ кастомный/мультиязычный у 84% (корпус 1728 ТЧ: multi 1032, custom-ru 417,
auto лишь 279), ToolTip у 24% (413). Компилятор хардкодил синоним=Split-CamelCase, Comment/ToolTip пусто.

DSL: значение ТЧ — массив колонок (синоним авто) ЛИБО объект {synonym, tooltip, comment, attributes/columns}
(по образцу реквизита: shorthand vs object). synonym/tooltip — ML.

- meta-compile (дуал-порт): нормализация ТЧ → {columns, synonym, tooltip, comment}; Emit-TabularSection
  параметризован (synonym через Emit-MLText, Comment/ToolTip из DSL).
- meta-decompile: ТЧ → объектная форма при кастомном синониме/подсказке/комментарии, иначе массив.
- spec §5.

Валидация: PS==PY; TS-категории ~0 (item 4566→6, ToolTip 2188→44); −10067 (89687→79620); регресс 37/37;
1С-cert зелёный. Остаток TabularSection>Synonym (~50) — станд. реквизит LineNumber ТЧ (отдельная категория).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:33:18 +03:00
Nick ShirokovandClaude Opus 4.8 72d6d47454 feat(meta-compile,meta-decompile): мультиязычный синоним объекта + терминология spec (v1.22/v0.7)
Раундтрип-находка: синоним САМОГО объекта (уровня справочника) мультиязычен у 950 справочников
корпуса (все erp: ru+en), но компилятор стрингифаил его ($synonym → ru-only), декомпилятор брал
Get-MLru (ru-only) → потеря en.

- meta-compile (дуал-порт): $synonym/synonym пробрасывается без стрингификации (строка ИЛИ {ru,en})
  → Emit-MLText (уже мультиязычный). Гард на плоском Description-fallback (ScheduledJob) для не-строки.
- meta-decompile: синоним объекта через Get-MLValue (строка ru | {ru,en}).
- spec: терминология «каталог» → «справочник» (тип Catalog = Справочник; файловая директория не тронута).

Валидация: PS==PY; byte-match erp-справочника; −3772 (93459→89687); регресс 37/37; 1С-cert зелёный.
Остаток item/ToolTip того уровня — синоним/подсказка ТЧ + xr:ToolTip станд. реквизита (следующие заходы).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:58:00 +03:00
Nick ShirokovandClaude Opus 4.8 5f53c3f02e feat(meta-compile,meta-decompile): батч object-свойств реквизита (консистентно с form) (v1.21/v0.6)
Раундтрип-кластер extra-свойств реквизита. Компилятор хардкодил дефолты, реальные значения
варьируются (корпус 32642 реквизита): FullTextSearch DontUse 10531, Comment 7993, Mask 1489,
Format/EditFormat 684/686, Use ForFolderAndItem 1299, CreateOnInput/QuickChoice/FillFromFillingValue/
DataHistory. По философии DSL — в объектную форму (shorthand несёт только частое: req/index/multiline).

Object-ключи (omit-on-default), имена согласованы с form-compile где применимо: comment, fullTextSearch,
mask, format/editFormat (ML), use, createOnInput, quickChoice, dataHistory, fillFromFillingValue,
passwordMode, choiceHistoryOnInput. Прощающий ввод: fillCheck→fillChecking (bool true→ShowError),
quickChoice bool (true→Use/false→DontUse). Emit-Attribute параметризован; декомпилятор переключается
на object-форму при любом непокрытом свойстве.

spec v2.6 §4.2 (полная таблица ключей); тест-кейс catalog-attr-props.
Валидация: PS==PY паритет; категории батча 0 (FullTextSearch/Comment/Mask/Format/Use/...);
полный прогон −68458 (161917→93459); регресс 37/37 (ps+py); 1С-cert зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 13:46:14 +03:00
Nick ShirokovandClaude Opus 4.8 53ed8c4489 fix(meta-compile): типы ValueStorage/UUID → v8:ValueStorage/v8:UUID + прощающие синонимы (v1.20)
Раундтрип-находка (класс-2): meta-compile эмитил ValueStorage как xs:base64Binary, а канон 1С —
v8:ValueStorage (2954 реквизита корпуса; xs:base64Binary как тип реквизита НЕ встречается — только
в Template.xml). UUID эмитился raw. Оба типа теперь эмитятся канонически.

Прощающий ввод (по идее: модель может ошибиться формой): base64Binary / ХранилищеЗначений /
ХранилищеЗначения → ValueStorage; УникальныйИдентификатор → UUID.

spec v2.5 (§3.1 типы + §3.3 синонимы); тест-кейс catalog-mixed-types расширен ValueStorage/UUID.
Валидация: v8:Type-категория 0 на таргет-выборке; регресс 36/36 (ps+py); 1С-cert зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 13:08:38 +03:00
Nick ShirokovandClaude Opus 4.8 253a4fbce0 feat(meta-compile,meta-decompile): предопределённые элементы Catalog (Ext/Predefined.xml) (v1.19/v0.5)
Раундтрип-находка (класс-3, непокрытый блок — крупнейшая категория ~56k). meta-compile вообще
не генерил Ext/Predefined.xml. DSL спроектирован по данным корпуса (9309 элементов) + совместно.

DSL `predefined` (массив; строка ИЛИ объект):
- строка "(Код) Имя [Наименование]": Имя обяз.; Наименование — нет [..] → авто(Split-CamelCase),
  [] → пусто (системные/placeholder, 29% корпуса), [текст] → задано;
- объект (группа/иерархия): name/code/description/isFolder/childItems + рус.синонимы
  имя/код/наименование/группа/подчиненные (прощающий ввод).
- Код: по codeType каталога (Number→xs:decimal, String→без типа), пусто→<Code/>.

meta-compile (дуал-порт): генерация Ext/Predefined.xml (root CatalogPredefinedItems + рекурсия Item).
Заодно фикс: Esc-XmlText для ТЕКСТА элемента (& < > без экранирования кавычек — в тексте 1С держит raw).
meta-decompile: захват predefined → компактный DSL (строка/объект). Фикс PS-ловушки распаковки
одноэлементного @() из if → папки с ОДНИМ ребёнком теряли его.
spec meta-dsl-spec.md v2.4 §7.1.2; тест-кейс catalog-predefined.

Валидация: PS==PY паритет; predefined-категории 0 (PredefinedData/Item/ChildItems/Description/Code);
полный прогон −56620 (218537→161917); регресс 36/36 (ps+py); 1С-cert зелёный (грузится в платформу).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:48:16 +03:00
Nick ShirokovandClaude Opus 4.8 58bc95264e feat(meta-compile,meta-decompile): мультиязычный ML для синонима и подсказки реквизита (v1.18/v0.4)
Раундтрип-находка (класс-1 синоним + класс-3 подсказка). Корпус: у реквизитов 21677 мультиязычных
синонимов (ru+en) и 16063 непустых подсказки (10689 мультиязычных). meta-compile писал ВСЁ ML только ru
(Emit-MLText брал строку) и хардкодил <ToolTip/> пустым → массовая потеря en + подсказок.

- meta-compile (дуал-порт): Emit-MLText/emit_mltext принимают строку (→ru) ИЛИ объект {lang:content}
  (→<v8:item> на язык, в порядке ключей) через новый Emit-MLItems/emit_ml_items. Parse object-форма
  реквизита пробрасывает synonym/tooltip без стрингификации; Emit-Attribute эмитит <ToolTip> из tooltip.
- meta-decompile: Get-MLValue → строка (ru-only) | {ru,en} (мультиязычно, порядок из XML); object-форма
  реквизита при кастомном синониме ИЛИ наличии подсказки.
- spec meta-dsl-spec.md v2.3: §4.2 (tooltip) + §4.4 ML-значения (строка/{ru,en}), консистентно с form-compile.
- тест-кейс catalog-attr-ml (мультиязычный синоним + подсказка, обе формы).

Валидация: PS==PY паритет; полный прогон −228748 строк (447285→218537, >половины); регресс 35/35 (ps+py);
1С-cert зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:18:01 +03:00
Nick ShirokovandClaude Opus 4.8 ed42d5e9cb refactor(meta-compile): единый data-driven эмиттер StandardAttributes вместо форка (v1.17)
Свернул два форка (Emit-StandardAttributes безусловный + Emit-StandardAttributesProfiled) в
ОДНУ функцию, поведение которой правят справочники, а не код:
- stdAttrConditionalTypes (пока {Catalog}) — типы, где блок только при DSL-ключе standardAttributes;
- stdAttrProfile[тип] — профиль материализованного блока.
Прочие 13 типов (не в справочниках) ведут себя ровно как раньше (безусловный all-default).

Убирает легаси-мост и будущий rename: вызов Catalog вернулся к каноничному
Emit-StandardAttributes(i,'Catalog'); нет второй функции/суффикса _profiled. Миграция типа =
+строчка в два справочника + переснять снэпшоты, кода не трогаем.

Поведение не изменилось: byte-match блока на синтетике, регресс 34/34 (ps+py), снэпшоты не тронуты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:21:39 +03:00
Nick ShirokovandClaude Opus 4.8 df5c1ee17d feat(meta-compile,meta-decompile): условный профильный StandardAttributes для Catalog (v1.16/v0.3)
Раундтрип-находка (класс-2 + новый DSL-блок). Правило платформы, выведенное из корпуса
(1596/1640 каталогов) и подтверждённое синтетикой: блок <StandardAttributes> материализуется
ТОЛЬКО при кастомизации ≥1 стандартного реквизита; 0 all-default блоков, 44 голых без блока.
При материализации платформа заполняет характеристический профиль (Owner{FC=ShowError,FFV=true},
Parent{FFV=true}, Description{FC=ShowError}), не зависящий от иерархии/владельца.

Было: meta-compile писал блок ВСЕГДА единым all-default шаблоном (неверные пер-атрибутные
дефолты) → ADDED-блок у 44 голых + потеря профиля/кастомизаций у 1596.

Стало:
- meta-compile (дуал-порт): блок эмитится только при наличии DSL-ключа `standardAttributes`
  (map реквизит→{synonym,fillChecking,fillFromFillingValue,fullTextSearch,dataHistory}); база =
  профиль типа (stdAttrProfile), поверх — override. Emit-StandardAttribute параметризован (ov).
  Эмиттер общий (Emit-StandardAttributesProfiled $type) — подключён только Catalog; прочие 13
  типов пока на старом безусловном пути (мигрируем при их пилоте).
- meta-decompile: захват `standardAttributes` как отклонений от профиля (блока нет → ключ опущен).
- spec meta-dsl-spec.md v2.2: раздел 7.1.1 (правило, профиль, формат).
- тест-кейс catalog-standard-attributes (профиль + override synonym + Code.fillChecking).

Валидация: byte-match блока на синтетике; полный прогон −27k строк (474339→447285), обвал
cascade ADDED 11538→1242; регресс 34/34 (ps+py); 1С-cert снэпшотов зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:12:56 +03:00
Nick ShirokovandClaude Opus 4.8 85e13e95f2 feat(meta-decompile): захват кастомного синонима реквизита (object-форма) + точное зеркало Split-CamelCase (v0.2)
Раундтрип-фикс (класс-1, декомпилятор): реквизит с синонимом ≠ авто теперь эмитится
в object-форме {name, type, synonym, flags} вместо shorthand (который синоним терял).
Синоним==авто → компактный shorthand как прежде.

Split-CamelWords переписан байт-в-байт под Split-CamelCase компилятора (прежняя версия
имела лишние правила: цифры, подряд-заглавные → риск ложных «синоним==авто»).

Срез ~12k строк на полном прогоне (1640 каталогов). Пометка: predefined-эмиссия
остаётся черновой; StandardAttributes-кастомизации — следующая категория (в обсуждении).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:37:32 +03:00
Nick ShirokovandClaude Opus 4.8 7000593561 fix(meta-compile): allowlist HierarchyType/CodeSeries — фантомное значение + пропуск (v1.15)
Раундтрип-находка (meta-roundtrip, класс-2): meta-compile падал на 90 каталогах
acc+erp из-за неполных/неверных enum-allowlist'ов:
- HierarchyType содержал ФАНТОМНОЕ "HierarchyItemsOnly" (в 1С такого значения нет;
  реальное — "HierarchyOfItems", 61 каталог) → компилятор сгенерил бы битый XML.
- CodeSeries без "WithinOwnerSubordination" (36 каталогов) → краш на валидном значении.

Значения сверены с корпусом (acc+erp 8.3.24). Фикс в обоих портах. После: dec-fail
90→0 на таргет-прогоне, регресс 33/33 (ps+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:17:24 +03:00
Nick ShirokovandClaude Opus 4.8 8b8620ae44 feat(meta-decompile): скрытый навык-декомпилятор объектов 1С (пилот Catalog)
Старт раундтрип-захода по meta-compile (как form/skd-roundtrip): scaffold скрытого
навыка meta-decompile (XML объекта → JSON-черновик формата meta-compile, инверс
компилятора, omit-on-default). Пилот — Catalog + общий слой (Properties, Attributes/
TabularSections shorthand с инверсией типов и флагов). Захват предопределённых из
соседнего Ext/Predefined.xml — ЧЕРНОВОЙ (DSL ещё не проработан).

disable-model-invocation (скрытый). Прочие типы → exit 3 (ring3). PY-зеркало — в конце.
Харнес раундтрипа и WORKFLOW — в debug/meta-roundtrip/ (gitignored).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:04:08 +03:00
2405 changed files with 139859 additions and 40108 deletions
+39 -6
View File
@@ -1,4 +1,4 @@
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
@@ -44,6 +44,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -80,10 +90,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -150,10 +163,17 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
Assert-EditAllowed $resolvedPath 'editable'
# --- Load XML with PreserveWhitespace ---
# NB: парсер XML по спецификации схлопывает CRLF в LF, а вставки ниже собираются с
# явным CRLF — поэтому EOL приводится к целевому в точке записи (см. финализацию).
$script:xmlDoc = New-Object System.Xml.XmlDocument
$script:xmlDoc.PreserveWhitespace = $true
$script:xmlDoc.Load($resolvedPath)
# Версия формата редактируемой конфигурации — создаваемые рядом файлы (Ext/HomePageWorkArea.xml)
# должны нести ту же версию, иначе в проекте окажутся файлы разных версий формата.
$script:formatVersion = $script:xmlDoc.DocumentElement.GetAttribute("version")
if (-not $script:formatVersion) { $script:formatVersion = "2.17" }
$script:addCount = 0
$script:removeCount = 0
$script:modifyCount = 0
@@ -196,7 +216,7 @@ Info "Configuration: $($script:objName)"
$script:typeOrder = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -212,7 +232,7 @@ $script:typeOrder = @(
$script:typeToDir = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
@@ -673,7 +693,9 @@ $bodyBlock$declarations
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
$caiPath = Join-Path $extDir "ClientApplicationInterface.xml"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($caiPath, $caiXml, $utf8Bom)
# Файл создаём мы — канон: CRLF в разделителях, без перевода строки в конце.
$caiXml = ($caiXml -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($caiPath, $caiXml.TrimEnd("`r", "`n"), $utf8Bom)
$script:modifyCount++
Info "Wrote panel layout: $caiPath"
}
@@ -700,6 +722,7 @@ $script:ruTypeMap = @{
"регистррасчёта" = "CalculationRegister"
"бизнеспроцесс" = "BusinessProcess"
"задача" = "Task"
"бот" = "Bot"
"планобмена" = "ExchangePlan"
"хранилищенастроек" = "SettingsStorage"
}
@@ -850,7 +873,7 @@ function Do-SetHomePage($valArg) {
$hpXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$($script:formatVersion)">
<WorkingAreaTemplate>$tmpl</WorkingAreaTemplate>
$leftXml
$rightXml
@@ -861,7 +884,9 @@ $rightXml
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
$hpPath = Join-Path $extDir "HomePageWorkArea.xml"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($hpPath, $hpXml, $utf8Bom)
# Файл создаём мы — канон: CRLF в разделителях, без перевода строки в конце.
$hpXml = ($hpXml -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($hpPath, $hpXml.TrimEnd("`r", "`n"), $utf8Bom)
$script:modifyCount++
Info "Wrote home page layout: $hpPath"
}
@@ -963,6 +988,14 @@ $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
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
+141 -21
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -12,6 +12,65 @@ import uuid as _uuid
from html import escape as html_escape
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
# сохраняется — от него зависит порядок эмиссии.
def _actual(self, key):
if not isinstance(key, str) or dict.__contains__(self, key):
return key
ci = self.__dict__.get('_ci')
if ci is None or len(ci) != len(self):
ci = {k.lower(): k for k in self if isinstance(k, str)}
self.__dict__['_ci'] = ci
return ci.get(key.lower(), key)
def __getitem__(self, key):
return dict.__getitem__(self, self._actual(key))
def __contains__(self, key):
return dict.__contains__(self, self._actual(key))
def get(self, key, default=None):
return dict.get(self, self._actual(key), default)
def pop(self, key, *default):
return dict.pop(self, self._actual(key), *default)
def __setitem__(self, key, value):
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
dict.__setitem__(self, self._actual(key), value)
def ci_json(obj):
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
if isinstance(obj, dict):
return CIDict((k, ci_json(v)) for k, v in obj.items())
if isinstance(obj, list):
return [ci_json(v) for v in obj]
return obj
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
@@ -33,6 +92,18 @@ def _sg_root_uuid(xml_path):
return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
@@ -72,6 +143,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
@@ -79,6 +153,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12):
if not d:
break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
@@ -182,7 +258,7 @@ XS_NS = "http://www.w3.org/2001/XMLSchema"
TYPE_ORDER = [
"Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
@@ -198,7 +274,7 @@ TYPE_ORDER = [
TYPE_TO_DIR = {
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
@@ -307,13 +383,50 @@ def parse_batch_value(val):
return items
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
def _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None → файл новый (сохранить текущее поведение)."""
try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → CRLF, канон #57)
if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_bom(tree, path):
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
@@ -326,7 +439,7 @@ def main():
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("-Value", default=None)
parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args()
args = ci_parse_args(parser)
if args.DefinitionFile and args.Operation:
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
@@ -357,6 +470,10 @@ def main():
tree = etree.parse(resolved_path, xml_parser)
xml_root = tree.getroot()
# Версия формата редактируемой конфигурации — создаваемые рядом файлы (Ext/HomePageWorkArea.xml)
# должны нести ту же версию, иначе в проекте окажутся файлы разных версий формата.
format_version = xml_root.get('version') or '2.17'
add_count = 0
remove_count = 0
modify_count = 0
@@ -705,7 +822,7 @@ def main():
layout = value
if isinstance(layout, str):
try:
layout = json.loads(layout)
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)
@@ -771,6 +888,7 @@ def main():
"регистррасчета": "CalculationRegister",
"регистррасчёта": "CalculationRegister",
"бизнеспроцесс": "BusinessProcess",
"бот": "Bot",
"задача": "Task", "планобмена": "ExchangePlan",
"хранилищенастроек": "SettingsStorage",
}
@@ -859,7 +977,7 @@ def main():
layout = value
if isinstance(layout, str):
try:
layout = json.loads(layout)
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)
@@ -905,7 +1023,7 @@ def main():
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">\r\n'
f'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">\r\n'
f'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
f'{left_xml}\r\n'
f'{right_xml}\r\n'
@@ -927,7 +1045,7 @@ def main():
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 = json.loads(fh.read())
ops = ci_json(json.loads(fh.read()))
if isinstance(ops, list):
operations = ops
else:
@@ -937,23 +1055,25 @@ def main():
for op in operations:
op_name = op.get("operation", args.Operation or "")
# PS сравнивает имя операции через switch, а он регистронезависим.
op_key = str(op_name).lower()
op_value = op.get("value", args.Value or "")
if op_name == "modify-property":
if op_key == "modify-property":
do_modify_property(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "add-childObject":
elif op_key == "add-childobject":
do_add_child_object(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "remove-childObject":
elif op_key == "remove-childobject":
do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "add-defaultRole":
elif op_key == "add-defaultrole":
do_add_default_role(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "remove-defaultRole":
elif op_key == "remove-defaultrole":
do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "set-defaultRoles":
elif op_key == "set-defaultroles":
do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "set-panels":
elif op_key == "set-panels":
do_set_panels(op_value)
elif op_name == "set-home-page":
elif op_key == "set-home-page":
do_set_home_page(op_value)
else:
print(f"Unknown operation: {op_name}", file=sys.stderr)
+3 -2
View File
@@ -1,4 +1,4 @@
# cf-info v1.3 — Compact summary of 1C configuration root
# cf-info v1.5 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
@@ -89,7 +89,7 @@ function Get-PropML([string]$propName) {
$typeOrder = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -105,6 +105,7 @@ $typeRuNames = @{
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
"Bot"="Боты"
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
+26 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-info v1.3 — Compact summary of 1C configuration root
# cf-info v1.5 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -12,6 +12,28 @@ from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
@@ -20,7 +42,7 @@ parser.add_argument("-Section", "-Name", choices=["home-page"], default=None, he
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file")
args = parser.parse_args()
args = ci_parse_args(parser)
# --- Output helper (collect all, paginate at the end) ---
lines_buf = []
@@ -95,7 +117,7 @@ def get_prop_ml(prop_name):
type_order = [
"Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
@@ -111,6 +133,7 @@ type_ru_names = {
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
"Bot": "Боты",
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
+70 -18
View File
@@ -1,4 +1,4 @@
# cf-init v1.2 — Create empty 1C configuration scaffold
# cf-init v1.11 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -7,10 +7,21 @@ param(
[string]$OutputDir = "src",
[string]$Version,
[string]$Vendor,
[string]$CompatibilityMode = "Version8_3_24"
[string]$CompatibilityMode = "Version8_3_24",
# Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима
# совместимости она не зависит: 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19,
# 8.3.27 — 2.20. Дефолт консервативный: 2.17 читается всеми поддерживаемыми платформами.
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
[string]$FormatVersion = "2.17"
)
$ErrorActionPreference = "Stop"
function Esc-XmlText {
param([string]$s)
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve output dir ---
@@ -38,6 +49,9 @@ $co6 = [guid]::NewGuid().ToString()
$co7 = [guid]::NewGuid().ToString()
# --- Mobile functionalities ---
# Версия формата как число — по ней ниже включаются вставки 2.21.
$is221 = (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221)
$mobileFuncs = @(
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
@@ -54,6 +68,9 @@ $mobileFuncs = @(
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
)
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.21 (8.5),
# последней в списке. На младших форматах платформа её не пишет.
if ($is221) { $mobileFuncs += ,@("TextToSpeech","false") }
$mobileXml = ""
foreach ($mf in $mobileFuncs) {
@@ -63,17 +80,43 @@ foreach ($mf in $mobileFuncs) {
# --- Synonym XML ---
$synonymXml = ""
if ($Synonym) {
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$(Esc-XmlText ($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
}
# --- Optional properties ---
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
# пишет <Vendor/>, а не <Vendor></Vendor>.
$vendorEl = if ($Vendor) { "<Vendor>$(Esc-XmlText ($Vendor))</Vendor>" } else { "<Vendor/>" }
$versionEl = if ($Version) { "<Version>$(Esc-XmlText ($Version))</Version>" } else { "<Version/>" }
# --- Свойства и пространство имён формата 2.21 (платформа 8.5) ---
# Значения и ПОЗИЦИИ сняты с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники,
# выгруженные с 8.3.27 и с 8.5.1, различаются ровно этим. Порядок важен — вставки идут
# на своё место, а не в конец.
$nl = "`r`n"
$f221AuxForms = ""; $f221WindowVariant = ""; $f221OpenVariant = ""; $f221Captions = ""; $f221Migration = ""
$palNs = ""
if ($is221) {
$palNs = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
# Скобки вокруг -join обязательны: без них `$nl + (массив) -join $nl` разбирается как
# `($nl + массив) -join $nl`, массив склеивается пробелами и все теги уезжают в одну строку.
$f221AuxForms = $nl + ((@(
"<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
"<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
"<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
"<AuxiliaryCollaborationSystemUsersChoiceForm/>"
) | ForEach-Object { "`t`t`t$_" }) -join $nl)
$f221WindowVariant = $nl + "`t`t`t<MainClientApplicationWindowInterfaceVariant>NavigationLeft</MainClientApplicationWindowInterfaceVariant>" +
$nl + "`t`t`t<ClientApplicationTheme>Auto</ClientApplicationTheme>"
$f221OpenVariant = $nl + "`t`t`t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs</ClientApplicationWindowsOpenVariant>"
$f221Captions = $nl + "`t`t`t<Caption/>" + $nl + "`t`t`t<ShortCaption/>"
$f221Migration = $nl + "`t`t`t<Version85InterfaceMigrationMode>DontUse</Version85InterfaceMigrationMode>"
}
# --- Configuration.xml ---
$cfgXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"$palNs xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
<Configuration uuid="$uuidCfg">
<InternalInfo>
<xr:ContainedObject>
@@ -106,7 +149,7 @@ $cfgXml = @"
</xr:ContainedObject>
</InternalInfo>
<Properties>
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
<Name>$(Esc-XmlText ($Name))</Name>
<Synonym>$synonymXml</Synonym>
<Comment/>
<NamePrefix/>
@@ -117,8 +160,8 @@ $cfgXml = @"
</UsePurposes>
<ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/>
<Vendor>$vendorXml</Vendor>
<Version>$versionXml</Version>
$vendorEl
$versionEl
<UpdateCatalogAddress/>
<IncludeHelpInContents>false</IncludeHelpInContents>
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
@@ -140,15 +183,15 @@ $cfgXml = @"
<DefaultDataHistoryChangeHistoryForm/>
<DefaultDataHistoryVersionDataForm/>
<DefaultDataHistoryVersionDifferencesForm/>
<DefaultCollaborationSystemUsersChoiceForm/>
<DefaultCollaborationSystemUsersChoiceForm/>$f221AuxForms
<RequiredMobileApplicationPermissions/>
<UsedMobileApplicationFunctionalities>$mobileXml
</UsedMobileApplicationFunctionalities>
<StandaloneConfigurationRestrictionRoles/>
<MobileApplicationURLs/>
<AllowedIncomingShareRequestTypes/>
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
<DefaultInterface/>
<AllowedIncomingShareRequestTypes/>$f221WindowVariant
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>$f221OpenVariant
<DefaultInterface/>$f221Captions
<DefaultStyle/>
<DefaultLanguage>Language.Русский</DefaultLanguage>
<BriefInformation/>
@@ -160,7 +203,7 @@ $cfgXml = @"
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
<ModalityUseMode>DontUse</ModalityUseMode>
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>$f221Migration
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
<DefaultConstantsForm/>
@@ -175,7 +218,7 @@ $cfgXml = @"
# --- Languages/Русский.xml ---
$langXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"$palNs xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
<Language uuid="$uuidLang">
<Properties>
<Name>Русский</Name>
@@ -235,11 +278,20 @@ if (-not (Test-Path $extDir)) {
# --- Write files with UTF-8 BOM ---
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile $cfgFile $cfgXml $enc
$langFile = Join-Path $langDir "Русский.xml"
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
Write-XmlFile $langFile $langXml $enc
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
Write-XmlFile $caiFile $caiXml $enc
# --- Output ---
Write-Host "[OK] Создана конфигурация: $Name"
+94 -21
View File
@@ -1,19 +1,55 @@
#!/usr/bin/env python3
# cf-init v1.2 — Create empty 1C configuration scaffold
# cf-init v1.11 — 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, uuid
import sys, os, argparse, re, uuid
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def esc_xml_text(s):
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -24,7 +60,12 @@ def main():
parser.add_argument('-Version', dest='Version', default='')
parser.add_argument('-Vendor', dest='Vendor', default='')
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
args = parser.parse_args()
# Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости:
# 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19, 8.3.27 — 2.20.
# Дефолт консервативный: 2.17 читается всеми платформами.
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
args = ci_parse_args(parser)
name = args.Name
synonym = args.Synonym if args.Synonym else name
@@ -49,6 +90,10 @@ def main():
co = [new_uuid() for _ in range(7)]
# --- Mobile functionalities ---
# Версия формата как число — по ней ниже включаются вставки 2.21.
_fm = re.match(r'^(\d+)\.(\d+)$', args.FormatVersion)
is_221 = bool(_fm) and int(_fm.group(1)) * 100 + int(_fm.group(2)) >= 221
mobile_funcs = [
("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"),
("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"),
@@ -65,6 +110,10 @@ def main():
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
]
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.21 (8.5),
# последней в списке. На младших форматах платформа её не пишет.
if is_221:
mobile_funcs.append(("TextToSpeech", "false"))
mobile_xml = ""
for func_name, func_use in mobile_funcs:
@@ -73,10 +122,12 @@ def main():
# --- Synonym XML ---
synonym_xml = ""
if synonym:
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
vendor_xml = esc_xml(vendor) if vendor else ""
version_xml = esc_xml(version) if version else ""
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
# пишет <Vendor/>, а не <Vendor></Vendor>.
vendor_el = f"<Vendor>{esc_xml_text(vendor)}</Vendor>" if vendor else "<Vendor/>"
version_el = f"<Version>{esc_xml_text(version)}</Version>" if version else "<Version/>"
class_ids = [
"9cd510cd-abfc-11d4-9434-004095e12fc7",
@@ -88,6 +139,28 @@ def main():
"fb282519-d103-4dd3-bc12-cb271d631dfc",
]
# Свойства и пространство имён формата 2.21 (платформа 8.5). Значения и ПОЗИЦИИ сняты
# с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники, выгруженные с 8.3.27 и
# с 8.5.1, различаются ровно этим. Порядок важен — вставки идут на своё место.
pal_ns = ""
f221_aux_forms = f221_window_variant = f221_open_variant = f221_captions = f221_migration = ""
if is_221:
pal_ns = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
f221_aux_forms = "\r\n" + "\r\n".join(
f"\t\t\t{t}" for t in (
"<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
"<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
"<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
"<AuxiliaryCollaborationSystemUsersChoiceForm/>"))
f221_window_variant = ("\r\n\t\t\t<MainClientApplicationWindowInterfaceVariant>NavigationLeft"
"</MainClientApplicationWindowInterfaceVariant>"
"\r\n\t\t\t<ClientApplicationTheme>Auto</ClientApplicationTheme>")
f221_open_variant = ("\r\n\t\t\t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs"
"</ClientApplicationWindowsOpenVariant>")
f221_captions = "\r\n\t\t\t<Caption/>\r\n\t\t\t<ShortCaption/>"
f221_migration = ("\r\n\t\t\t<Version85InterfaceMigrationMode>DontUse"
"</Version85InterfaceMigrationMode>")
contained_objects = ""
for i in range(7):
contained_objects += f"""\t\t\t<xr:ContainedObject>
@@ -96,12 +169,12 @@ def main():
\t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"{pal_ns} xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
\t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Name>{esc_xml_text(name)}</Name>
\t\t\t<Synonym>{synonym_xml}</Synonym>
\t\t\t<Comment/>
\t\t\t<NamePrefix/>
@@ -112,8 +185,8 @@ def main():
\t\t\t</UsePurposes>
\t\t\t<ScriptVariant>Russian</ScriptVariant>
\t\t\t<DefaultRoles/>
\t\t\t<Vendor>{vendor_xml}</Vendor>
\t\t\t<Version>{version_xml}</Version>
\t\t\t{vendor_el}
\t\t\t{version_el}
\t\t\t<UpdateCatalogAddress/>
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
@@ -135,15 +208,15 @@ def main():
\t\t\t<DefaultDataHistoryChangeHistoryForm/>
\t\t\t<DefaultDataHistoryVersionDataForm/>
\t\t\t<DefaultDataHistoryVersionDifferencesForm/>
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/>
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/>{f221_aux_forms}
\t\t\t<RequiredMobileApplicationPermissions/>
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
\t\t\t</UsedMobileApplicationFunctionalities>
\t\t\t<StandaloneConfigurationRestrictionRoles/>
\t\t\t<MobileApplicationURLs/>
\t\t\t<AllowedIncomingShareRequestTypes/>
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
\t\t\t<DefaultInterface/>
\t\t\t<AllowedIncomingShareRequestTypes/>{f221_window_variant}
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>{f221_open_variant}
\t\t\t<DefaultInterface/>{f221_captions}
\t\t\t<DefaultStyle/>
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
\t\t\t<BriefInformation/>
@@ -155,7 +228,7 @@ def main():
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>{f221_migration}
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
\t\t\t<DefaultConstantsForm/>
@@ -168,7 +241,7 @@ def main():
# --- Languages/Русский.xml ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"{pal_ns} xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
\t<Language uuid="{uuid_lang}">
\t\t<Properties>
\t\t\t<Name>Русский</Name>
@@ -217,11 +290,11 @@ def main():
os.makedirs(ext_dir, exist_ok=True)
# --- Write files ---
write_utf8_bom(cfg_file, cfg_xml)
write_xml_file(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml)
write_xml_file(lang_file, lang_xml)
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
write_utf8_bom(cai_file, cai_xml)
write_xml_file(cai_file, cai_xml)
print(f"[OK] Создана конфигурация: {name}")
print(f" Каталог: {output_dir}")
@@ -1,4 +1,4 @@
# cf-validate v1.3 — Validate 1C configuration root structure
# cf-validate v1.6 — Validate 1C configuration root structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -104,11 +104,11 @@ $validClassIds = @(
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
)
# 44 types in canonical order
# 45 types in canonical order
$childObjectTypes = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -125,6 +125,7 @@ $childTypeDirMap = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
"Bot"="Bots"
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
@@ -204,8 +205,9 @@ if ($root.NamespaceURI -ne $expectedNs) {
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
}
# Must have Configuration child
@@ -1,10 +1,32 @@
#!/usr/bin/env python3
# cf-validate v1.3 — Validate 1C configuration XML structure
# cf-validate v1.6 — 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
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
NS = {
'md': 'http://v8.1c.ru/8.3/MDClasses',
'v8': 'http://v8.1c.ru/8.1/data/core',
@@ -33,11 +55,11 @@ VALID_CLASS_IDS = [
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
]
# 44 types in canonical order
# 45 types in canonical order
CHILD_OBJECT_TYPES = [
'Language', 'Subsystem', 'StyleItem', 'Style',
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
@@ -54,6 +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',
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
@@ -169,7 +192,7 @@ def main():
parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args()
args = ci_parse_args(parser)
config_path = args.ConfigPath
max_errors = args.MaxErrors
@@ -231,8 +254,9 @@ def main():
version = root.get('version', '')
if not version:
r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20', '2.21'):
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'):
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
# Must have Configuration child
cfg_node = None
-1
View File
@@ -41,7 +41,6 @@ allowed-tools:
- `Enum.ВидыОплат` — перечисление
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
Поддерживаются все 44 типа объектов конфигурации.
### Заимствование форм
+105 -10
View File
@@ -1,4 +1,4 @@
# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.20 — Borrow objects from configuration into extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$ExtensionPath,
@@ -125,7 +125,7 @@ $childTypeDirMap = @{
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"
"HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"CommonAttribute"="CommonAttributes"; "Style"="Styles"
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "Language"="Languages"
}
# --- 4b. Russian synonym → English type ---
@@ -153,7 +153,7 @@ $synonymMap = @{
$script:typeOrder = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -285,14 +285,36 @@ $script:generatedTypes = @{
"DefinedType" = @(
@{ prefix = "DefinedType"; category = "DefinedType" }
)
"Sequence" = @(
@{ prefix = "SequenceRecord"; category = "Record" }
@{ prefix = "SequenceManager"; category = "Manager" }
@{ prefix = "SequenceRecordSet"; category = "RecordSet" }
)
"FilterCriterion" = @(
@{ prefix = "FilterCriterionManager"; category = "Manager" }
@{ prefix = "FilterCriterionList"; category = "List" }
)
"SettingsStorage" = @(
@{ prefix = "SettingsStorageManager"; category = "Manager" }
)
"IntegrationService" = @(
@{ prefix = "IntegrationServiceManager"; category = "Manager" }
)
"WSReference" = @(
@{ prefix = "WSReferenceManager"; category = "Manager" }
)
}
# Types that need ChildObjects element
# Types that need ChildObjects element — fallback when the source object cannot be probed.
# The platform emits <ChildObjects> for every container type even when empty, and rejects
# the file without it ("ожидаемое ChildObjects"); primary signal is the source object itself.
$typesWithChildObjects = @(
"Catalog","Document","ExchangePlan","ChartOfAccounts",
"ChartOfCharacteristicTypes","ChartOfCalculationTypes",
"BusinessProcess","Task","Enum",
"InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister"
"InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister",
"DataProcessor","Report","DocumentJournal","FilterCriterion","SettingsStorage",
"Sequence","HTTPService","WebService","IntegrationService","Subsystem"
)
# CommonModule properties to copy from source
@@ -346,9 +368,20 @@ function Expand-SelfClosingElement($container, $parentIndent) {
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) {
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
@@ -363,6 +396,20 @@ $script:formatVersion = Detect-FormatVersion $extDir
# --- 8. Namespaces declaration for object XML ---
$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"'
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
if ((Get-FormatRank $script:formatVersion) -ge 221) {
$script:xmlnsDecl = $script:xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
# --- 9. Parse -Object into items ---
$items = @()
foreach ($part in $Object.Split(";;")) {
@@ -454,6 +501,9 @@ function Read-SourceObject {
}
}
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
$srcProps["__HasChildObjects"] = ($srcEl.SelectSingleNode("md:ChildObjects", $srcNs) -ne $null)
return @{
Uuid = $srcUuid
Properties = $srcProps
@@ -796,11 +846,22 @@ function Borrow-Form {
}
}
# Extract the <Form ...> opening tag from source text (preserves namespace declarations)
# Открывающий тег <Form ...> берём из исходной формы — ради её объявлений пространств имён,
# но version подставляем СВОЮ: форма обязана нести версию расширения, иначе платформа
# отвергает импорт (форма 2.13 внутри расширения 2.17). Раньше тег копировался целиком,
# и версия источника молча побеждала.
$xmlDecl = '<?xml version="1.0" encoding="UTF-8"?>'
$formTag = "<Form version=`"${formVersion}`">"
if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] }
if ($srcFormContent -match '(<Form[^>]*>)') { $formTag = $Matches[1] }
if ($srcFormContent -match '(<Form[^>]*>)') {
$srcTag = $Matches[1]
$srcNs = $srcTag -replace '^<Form\s*', '' -replace '\s*/?>$', '' -replace '\s*version="[^"]*"', ''
# 2.21 (8.5): пространство палитры. Место строгое — после lf, перед style.
if ((Get-FormatRank $formVersion) -ge 221 -and $srcNs -notmatch 'xmlns:pal=') {
$srcNs = $srcNs -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
$formTag = if ($srcNs) { "<Form $srcNs version=`"${formVersion}`">" } else { "<Form version=`"${formVersion}`">" }
}
# Build output Form.xml
$formXmlSb = New-Object System.Text.StringBuilder
@@ -889,7 +950,15 @@ function Borrow-Form {
New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null
}
$formXmlFile = Join-Path $formXmlDir "Form.xml"
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
# Здесь источник не XmlWriter, а OuterXml исходного документа — спацовывает так же.
$formXmlText = $formXmlSb.ToString()
$formXmlText = [regex]::Replace($formXmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Файл создаём мы — канон выгрузки: CRLF в разделителях строк.
$formXmlText = ($formXmlText -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($formXmlFile, $formXmlText, $enc)
Info " Created: $formXmlFile"
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
@@ -995,8 +1064,16 @@ function Register-FormInObject {
$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)
Info " Registered form in: $objFile"
}
@@ -1385,7 +1462,17 @@ function Merge-AttributesIntoObject {
# Insert attributes before </ChildObjects>
$text3 = $text3 -replace '</ChildObjects>', "${allAttrXml}`r`n`t`t</ChildObjects>"
# Пустой элемент: 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)
Info " Merged $added attribute(s) into: $objFile"
}
@@ -1669,7 +1756,7 @@ function Build-BorrowedObjectXml {
$sb.AppendLine("`t`t</Properties>") | Out-Null
# ChildObjects (for types that need it)
if ($typesWithChildObjects -contains $typeName) {
if ($sourceProps["__HasChildObjects"] -or ($typesWithChildObjects -contains $typeName)) {
$sb.AppendLine("`t`t<ChildObjects/>") | Out-Null
}
@@ -1849,8 +1936,16 @@ $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)
Info "Saved: $extResolvedPath"
+173 -29
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.20 — Borrow objects from configuration into extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -9,6 +9,28 @@ import sys
import uuid
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
@@ -80,6 +102,7 @@ CHILD_TYPE_DIR_MAP = {
"XDTOPackage": "XDTOPackages", "WebService": "WebServices",
"HTTPService": "HTTPServices", "WSReference": "WSReferences",
"CommonAttribute": "CommonAttributes", "Style": "Styles",
"Bot": "Bots", "Language": "Languages",
}
SYNONYM_MAP = {
@@ -123,7 +146,7 @@ SYNONYM_MAP = {
TYPE_ORDER = [
"Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
@@ -254,13 +277,36 @@ GENERATED_TYPES = {
"DefinedType": [
{"prefix": "DefinedType", "category": "DefinedType"},
],
"Sequence": [
{"prefix": "SequenceRecord", "category": "Record"},
{"prefix": "SequenceManager", "category": "Manager"},
{"prefix": "SequenceRecordSet", "category": "RecordSet"},
],
"FilterCriterion": [
{"prefix": "FilterCriterionManager", "category": "Manager"},
{"prefix": "FilterCriterionList", "category": "List"},
],
"SettingsStorage": [
{"prefix": "SettingsStorageManager", "category": "Manager"},
],
"IntegrationService": [
{"prefix": "IntegrationServiceManager", "category": "Manager"},
],
"WSReference": [
{"prefix": "WSReferenceManager", "category": "Manager"},
],
}
# Types that need ChildObjects element — fallback when the source object cannot be probed.
# The platform emits <ChildObjects> for every container type even when empty, and rejects
# the file without it ("expected ChildObjects"); primary signal is the source object itself.
TYPES_WITH_CHILD_OBJECTS = [
"Catalog", "Document", "ExchangePlan", "ChartOfAccounts",
"ChartOfCharacteristicTypes", "ChartOfCalculationTypes",
"BusinessProcess", "Task", "Enum",
"InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister",
"DataProcessor", "Report", "DocumentJournal", "FilterCriterion", "SettingsStorage",
"Sequence", "HTTPService", "WebService", "IntegrationService", "Subsystem",
]
COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"]
@@ -286,6 +332,16 @@ XMLNS_DECL = (
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:
@@ -300,6 +356,23 @@ def detect_format_version(d):
return "2.17"
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def apply_pal_ns(format_version):
"""2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
дописать в конец нельзя."""
global XMLNS_DECL
if format_rank(format_version) >= 221:
XMLNS_DECL = XMLNS_DECL.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
def get_child_indent(container):
if container.text and "\n" in container.text:
after_nl = container.text.rsplit("\n", 1)[-1]
@@ -349,19 +422,71 @@ def expand_self_closing(container, parent_indent):
container.text = "\r\n" + parent_indent
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
def _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None → файл новый (сохранить текущее поведение)."""
try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → CRLF, канон #57)
if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_bom(tree, path):
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def save_text_bom(path, text):
with open(path, "w", encoding="utf-8-sig") as fh:
fh.write(text)
def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
Только для файлов, которые СОЗДАЁМ: правка существующего наследует его стиль.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def new_guid():
@@ -376,7 +501,7 @@ def main():
parser.add_argument("-ConfigPath", required=True)
parser.add_argument("-Object", required=True)
parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None)
args = parser.parse_args()
args = ci_parse_args(parser)
# --- 1. Resolve paths ---
ext_path = args.ExtensionPath
@@ -412,6 +537,7 @@ def main():
cfg_dir = os.path.dirname(cfg_resolved)
format_version = detect_format_version(ext_dir)
apply_pal_ns(format_version)
# --- 2. Load extension Configuration.xml ---
xml_parser = etree.XMLParser(remove_blank_text=False)
@@ -500,6 +626,9 @@ def main():
type_xml = etree.tostring(type_node, encoding="unicode")
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
src_props["__HasChildObjects"] = src_el.find(f"{{{MD_NS}}}ChildObjects") is not None
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
def read_source_form_uuid(type_name, obj_name, form_name):
@@ -576,7 +705,7 @@ def main():
lines.append("\t\t</Properties>")
if type_name in TYPES_WITH_CHILD_OBJECTS:
if source_props.get("__HasChildObjects") or type_name in TYPES_WITH_CHILD_OBJECTS:
lines.append("\t\t<ChildObjects/>")
lines.append(f"\t</{type_name}>")
@@ -935,7 +1064,9 @@ def main():
warn(f"Cannot merge attributes: {obj_file} not found")
return
with open(obj_file, "r", encoding="utf-8-sig") as fh:
# newline="" => без трансляции: иначе CRLF молча схлопнется в LF при чтении
# и файл будет переписан в LF.
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
obj_content = fh.read()
# Collect existing attribute names for dedup (text-based)
@@ -957,7 +1088,7 @@ def main():
obj_content = re.sub(r'<ChildObjects\s*/>', f"<ChildObjects>{all_attr_xml}\r\n\t\t</ChildObjects>", obj_content)
else:
obj_content = obj_content.replace("</ChildObjects>", f"{all_attr_xml}\r\n\t\t</ChildObjects>")
save_text_bom(obj_file, obj_content)
write_utf8_bom(obj_file, obj_content)
info(f" Merged {added} attribute(s) into: {obj_file}")
# --- 11h. Borrow main attribute orchestrator ---
@@ -994,7 +1125,9 @@ def main():
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
# Read existing object XML (needed for dedup + enrichment)
with open(obj_file, "r", encoding="utf-8-sig") as fh:
# newline="" => без трансляции: иначе CRLF молча схлопнется в LF при чтении
# и файл будет переписан в LF.
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
obj_content = fh.read()
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
@@ -1042,7 +1175,7 @@ def main():
f"<ChildObjects>{existing_inner}\r\n{adopted_content}\r\n\t\t</ChildObjects>"
)
save_text_bom(obj_file, obj_content)
write_utf8_bom(obj_file, obj_content)
info(f" Enriched object: {obj_file}")
# Step 4: Collect all reference types and borrow as shells
@@ -1071,7 +1204,7 @@ def main():
target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[rt["TypeName"]])
os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{rt['ObjName']}.xml")
save_text_bom(target_file, borrowed_xml)
write_xml_file(target_file, borrowed_xml)
add_to_child_objects(rt["TypeName"], rt["ObjName"])
borrowed_files.append(target_file)
info(f" Auto-borrowed: {rt['TypeName']}.{rt['ObjName']}")
@@ -1136,7 +1269,7 @@ def main():
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
os.makedirs(t_target_dir, exist_ok=True)
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
save_text_bom(t_target_file, t_borrowed_xml)
write_xml_file(t_target_file, t_borrowed_xml)
add_to_child_objects(target_type_name, target_obj_name)
borrowed_files.append(t_target_file)
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
@@ -1160,7 +1293,7 @@ def main():
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
os.makedirs(s_target_dir, exist_ok=True)
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
save_text_bom(s_target_file, s_borrowed_xml)
write_xml_file(s_target_file, s_borrowed_xml)
add_to_child_objects(srt["TypeName"], srt["ObjName"])
borrowed_files.append(s_target_file)
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
@@ -1217,7 +1350,7 @@ def main():
os.makedirs(form_meta_dir, exist_ok=True)
form_meta_file = os.path.join(form_meta_dir, f"{form_name}.xml")
save_text_bom(form_meta_file, "\n".join(form_meta_lines))
write_xml_file(form_meta_file, "\n".join(form_meta_lines))
info(f" Created: {form_meta_file}")
# 5. Generate Form.xml with BaseForm
@@ -1303,7 +1436,7 @@ def main():
target_dir = os.path.join(ext_dir, "CommonPictures")
os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{pic_name}.xml")
save_text_bom(target_file, borrowed_xml)
write_xml_file(target_file, borrowed_xml)
add_to_child_objects("CommonPicture", pic_name)
auto_borrowed_pics.append(pic_name)
borrowed_files.append(target_file)
@@ -1352,7 +1485,7 @@ def main():
target_dir = os.path.join(ext_dir, "StyleItems")
os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{style_name}.xml")
save_text_bom(target_file, borrowed_xml)
write_xml_file(target_file, borrowed_xml)
add_to_child_objects("StyleItem", style_name)
borrowed_files.append(target_file)
info(f" Auto-borrowed: StyleItem.{style_name}")
@@ -1416,14 +1549,17 @@ def main():
target_dir = os.path.join(ext_dir, "Enums")
os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{enum_name}.xml")
save_text_bom(target_file, borrowed_xml)
write_xml_file(target_file, borrowed_xml)
add_to_child_objects("Enum", enum_name)
borrowed_files.append(target_file)
info(f" Auto-borrowed: Enum.{enum_name} (with {len(ev_xmls)} EnumValue(s))")
else:
warn(f" Enum.{enum_name} not found in source config")
# Extract the <Form ...> opening tag from source text
# Открывающий тег <Form ...> берём из исходной формы — ради её объявлений пространств
# имён, но version подставляем СВОЮ: форма обязана нести версию расширения, иначе
# платформа отвергает импорт (форма 2.13 внутри расширения 2.17). Раньше тег
# копировался целиком, и версия источника молча побеждала.
xml_decl = '<?xml version="1.0" encoding="UTF-8"?>'
form_tag = f'<Form version="{form_version}">'
m_decl = re.search(r'^(<\?xml[^?]*\?>)', src_form_content)
@@ -1431,7 +1567,15 @@ def main():
xml_decl = m_decl.group(1)
m_tag = re.search(r'(<Form[^>]*>)', src_form_content)
if m_tag:
form_tag = m_tag.group(1)
src_ns = re.sub(r'^<Form\s*', '', m_tag.group(1))
src_ns = re.sub(r'\s*/?>$', '', src_ns)
src_ns = re.sub(r'\s*version="[^"]*"', '', src_ns)
# 2.21 (8.5): пространство палитры. Место строгое — после lf, перед style.
if format_rank(form_version) >= 221 and 'xmlns:pal=' not in src_ns:
src_ns = src_ns.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
form_tag = f'<Form {src_ns} version="{form_version}">' if src_ns else f'<Form version="{form_version}">'
# Build output
parts = []
@@ -1510,7 +1654,7 @@ def main():
form_xml_dir = os.path.join(form_meta_dir, form_name, "Ext")
os.makedirs(form_xml_dir, exist_ok=True)
form_xml_file = os.path.join(form_xml_dir, "Form.xml")
save_text_bom(form_xml_file, "".join(parts))
write_xml_file(form_xml_file, "".join(parts))
info(f" Created: {form_xml_file}")
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
@@ -1521,7 +1665,7 @@ def main():
if os.path.isfile(module_bsl_file):
info(" Preserved existing Module.bsl")
else:
save_text_bom(module_bsl_file, "")
write_utf8_bom(module_bsl_file, "")
info(f" Created: {module_bsl_file}")
# 7. Register form in parent object ChildObjects
@@ -1594,7 +1738,7 @@ def main():
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")
save_text_bom(target_file, borrowed_xml)
write_xml_file(target_file, borrowed_xml)
info(f" Created: {target_file}")
add_to_child_objects(type_name, obj_name)
@@ -1621,7 +1765,7 @@ def main():
os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{obj_name}.xml")
save_text_bom(target_file, borrowed_xml)
write_xml_file(target_file, borrowed_xml)
info(f" Created: {target_file}")
add_to_child_objects(type_name, obj_name)
+4 -2
View File
@@ -1,4 +1,4 @@
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -49,7 +49,9 @@ $childTypeDirMap = @{
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
"CommonAttribute"="CommonAttributes"
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "XDTOPackage"="XDTOPackages"
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"Bot"="Bots"
}
# --- Parse extension Configuration.xml ---
+30 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -8,6 +8,28 @@ import re
import sys
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# --- Namespace maps ---
MD_NSMAP = {
@@ -60,6 +82,12 @@ CHILD_TYPE_DIR_MAP = {
"Sequence": "Sequences",
"IntegrationService": "IntegrationServices",
"CommonAttribute": "CommonAttributes",
"Style": "Styles",
"XDTOPackage": "XDTOPackages",
"WebService": "WebServices",
"HTTPService": "HTTPServices",
"WSReference": "WSReferences",
"Bot": "Bots",
}
@@ -468,7 +496,7 @@ def main():
parser.add_argument("-ExtensionPath", required=True, help="Path to extension dump root")
parser.add_argument("-ConfigPath", required=True, help="Path to base config dump root")
parser.add_argument("-Mode", choices=["A", "B"], default="A", help="A=overview, B=transfer check")
args = parser.parse_args()
args = ci_parse_args(parser)
extension_path = args.ExtensionPath
config_path = args.ConfigPath
+58 -18
View File
@@ -1,4 +1,4 @@
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
# cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -16,6 +16,12 @@ param(
)
$ErrorActionPreference = "Stop"
function Esc-XmlText {
param([string]$s)
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Default NamePrefix ---
@@ -121,20 +127,23 @@ $co7 = [guid]::NewGuid().ToString()
# --- Synonym XML ---
$synonymXml = ""
if ($Synonym) {
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$(Esc-XmlText ($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
}
# --- Optional properties ---
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
# пишет <Vendor/>, а не <Vendor></Vendor>.
$vendorEl = if ($Vendor) { "<Vendor>$(Esc-XmlText ($Vendor))</Vendor>" } else { "<Vendor/>" }
$versionEl = if ($Version) { "<Version>$(Esc-XmlText ($Version))</Version>" } else { "<Version/>" }
# --- Role name ---
$roleName = "${NamePrefix}ОсновнаяРоль"
# --- DefaultRoles XML ---
$defaultRolesXml = ""
# Элемент целиком: без роли Конфигуратор пишет <DefaultRoles/>, а не пустую пару.
$defaultRolesEl = "<DefaultRoles/>"
if (-not $NoRole) {
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t"
$defaultRolesEl = "<DefaultRoles>`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t</DefaultRoles>"
}
# --- ChildObjects ---
@@ -144,10 +153,32 @@ if (-not $NoRole) {
}
$childObjectsXml += "`r`n`t`t"
# Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
# Caption/ShortCaption — свойства корня из того же формата 2.21, между Version и
# DefaultLanguage (позиция снята с выгрузки расширения из базы 8.5).
$f221Captions = ""
if ((Get-FormatRank $formatVersion) -ge 221) {
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
$f221Captions = "`r`n`t`t`t<Caption/>`r`n`t`t`t<ShortCaption/>"
}
# --- Configuration.xml ---
$cfgXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
<MetaDataObject $xmlnsDecl version="$formatVersion">
<Configuration uuid="$uuidCfg">
<InternalInfo>
<xr:ContainedObject>
@@ -181,21 +212,21 @@ $cfgXml = @"
</InternalInfo>
<Properties>
<ObjectBelonging>Adopted</ObjectBelonging>
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
<Name>$(Esc-XmlText ($Name))</Name>
<Synonym>$synonymXml</Synonym>
<Comment/>
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
<NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix>
<NamePrefix>$(Esc-XmlText ($NamePrefix))</NamePrefix>
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
<DefaultRunMode>ManagedApplication</DefaultRunMode>
<UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
</UsePurposes>
<ScriptVariant>Russian</ScriptVariant>
<DefaultRoles>$defaultRolesXml</DefaultRoles>
<Vendor>$vendorXml</Vendor>
<Version>$versionXml</Version>
$defaultRolesEl
$vendorEl
$versionEl$f221Captions
<DefaultLanguage>Language.Русский</DefaultLanguage>
<BriefInformation/>
<DetailedInformation/>
@@ -212,7 +243,7 @@ $cfgXml = @"
# --- Languages/Русский.xml (adopted format) ---
$langXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
<MetaDataObject $xmlnsDecl version="$formatVersion">
<Language uuid="$uuidLang">
<InternalInfo/>
<Properties>
@@ -229,10 +260,10 @@ $langXml = @"
# --- Role XML ---
$roleXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
<MetaDataObject $xmlnsDecl version="$formatVersion">
<Role uuid="$uuidRole">
<Properties>
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
<Name>$(Esc-XmlText ($roleName))</Name>
<Synonym/>
<Comment/>
</Properties>
@@ -252,9 +283,18 @@ if (-not (Test-Path $langDir)) {
# --- Write files with UTF-8 BOM ---
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile $cfgFile $cfgXml $enc
$langFile = Join-Path $langDir "Русский.xml"
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
Write-XmlFile $langFile $langXml $enc
# --- Role ---
if (-not $NoRole) {
@@ -263,7 +303,7 @@ if (-not $NoRole) {
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
}
$roleFile = Join-Path $roleDir "$roleName.xml"
[System.IO.File]::WriteAllText($roleFile, $roleXml, $enc)
Write-XmlFile $roleFile $roleXml $enc
}
# --- Output ---
+103 -22
View File
@@ -1,20 +1,62 @@
#!/usr/bin/env python3
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
# cfe-init v1.10 — 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, argparse, uuid
import sys, os, re, argparse, uuid
from xml.etree import ElementTree as ET
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def esc_xml_text(s):
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -29,7 +71,7 @@ def main():
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
parser.add_argument('-ConfigPath', dest='ConfigPath', default=None)
parser.add_argument('-NoRole', dest='NoRole', action='store_true')
args = parser.parse_args()
args = ci_parse_args(parser)
name = args.Name
synonym = args.Synonym if args.Synonym else name
@@ -126,18 +168,23 @@ def main():
# --- Synonym XML ---
synonym_xml = ""
if synonym:
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
vendor_xml = esc_xml(vendor) if vendor else ""
version_xml = esc_xml(version) if version else ""
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
# пишет <Vendor/>, а не <Vendor></Vendor>.
vendor_el = f"<Vendor>{esc_xml_text(vendor)}</Vendor>" if vendor else "<Vendor/>"
version_el = f"<Version>{esc_xml_text(version)}</Version>" if version else "<Version/>"
# --- Role name ---
role_name = f"{name_prefix}ОсновнаяРоль"
# --- DefaultRoles XML ---
default_roles_xml = ""
# Элемент целиком: без роли Конфигуратор пишет <DefaultRoles/>, а не пустую пару.
default_roles_el = "<DefaultRoles/>"
if not args.NoRole:
default_roles_xml = f'\r\n\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>\r\n\t\t\t'
default_roles_el = ('<DefaultRoles>\r\n\t\t\t\t'
f'<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>'
'\r\n\t\t\t</DefaultRoles>')
# --- ChildObjects ---
child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>"
@@ -156,6 +203,40 @@ def main():
]
contained_objects = ""
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
xmlns_decl = (
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
# Caption/ShortCaption — свойства корня из того же формата 2.21, между Version и
# DefaultLanguage (позиция снята с выгрузки расширения из базы 8.5).
f221_captions = ""
if format_rank(format_version) >= 221:
xmlns_decl = xmlns_decl.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
f221_captions = "\r\n\t\t\t<Caption/>\r\n\t\t\t<ShortCaption/>"
for i in range(7):
contained_objects += f"""\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
@@ -163,27 +244,27 @@ def main():
\t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
<MetaDataObject {xmlns_decl} version="{format_version}">
\t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Name>{esc_xml_text(name)}</Name>
\t\t\t<Synonym>{synonym_xml}</Synonym>
\t\t\t<Comment/>
\t\t\t<ConfigurationExtensionPurpose>{purpose}</ConfigurationExtensionPurpose>
\t\t\t<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
\t\t\t<NamePrefix>{esc_xml(name_prefix)}</NamePrefix>
\t\t\t<NamePrefix>{esc_xml_text(name_prefix)}</NamePrefix>
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
\t\t\t<UsePurposes>
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
\t\t\t</UsePurposes>
\t\t\t<ScriptVariant>Russian</ScriptVariant>
\t\t\t<DefaultRoles>{default_roles_xml}</DefaultRoles>
\t\t\t<Vendor>{vendor_xml}</Vendor>
\t\t\t<Version>{version_xml}</Version>
\t\t\t{default_roles_el}
\t\t\t{vendor_el}
\t\t\t{version_el}{f221_captions}
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
\t\t\t<BriefInformation/>
\t\t\t<DetailedInformation/>
@@ -198,7 +279,7 @@ def main():
# --- Languages/Русский.xml (adopted format) ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
<MetaDataObject {xmlns_decl} version="{format_version}">
\t<Language uuid="{uuid_lang}">
\t\t<InternalInfo/>
\t\t<Properties>
@@ -213,10 +294,10 @@ def main():
# --- Role XML ---
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
<MetaDataObject {xmlns_decl} version="{format_version}">
\t<Role uuid="{uuid_role}">
\t\t<Properties>
\t\t\t<Name>{esc_xml(role_name)}</Name>
\t\t\t<Name>{esc_xml_text(role_name)}</Name>
\t\t\t<Synonym/>
\t\t\t<Comment/>
\t\t</Properties>
@@ -229,9 +310,9 @@ def main():
os.makedirs(lang_dir, exist_ok=True)
# --- Write files ---
write_utf8_bom(cfg_file, cfg_xml)
write_xml_file(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml)
write_xml_file(lang_file, lang_xml)
# --- Role ---
role_file = None
@@ -239,7 +320,7 @@ def main():
role_dir = os.path.join(output_dir, "Roles")
os.makedirs(role_dir, exist_ok=True)
role_file = os.path.join(role_dir, f"{role_name}.xml")
write_utf8_bom(role_file, role_xml)
write_xml_file(role_file, role_xml)
# --- Output ---
print(f"[OK] Создано расширение: {name}")
+95 -28
View File
@@ -1,7 +1,7 @@
---
name: cfe-patch-method
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
description: Генерация и актуализация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после, вместо оригинала, изменить его тело (ИзменениеИКонтроль) — или актуализировать перехватчик после изменения оригинала
argument-hint: -ExtensionPath <path> -ConfigPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
allowed-tools:
- Bash
- Read
@@ -10,22 +10,31 @@ allowed-tools:
# /cfe-patch-method — Генерация перехватчика метода
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий.
Читает оригинал метода из конфигурации-источника и генерирует `.bsl`-перехватчик заимствованного объекта: с правильной директивой контекста, полной сигнатурой, обрамляющими инструкциями препроцессора и областями. Для `ИзменениеИКонтроль` копирует всё тело оригинала. Создаёт файл модуля, дописывает в существующий или актуализирует уже заимствованный метод.
## Предусловие
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры.
Объект должен быть заимствован в расширение (`/cfe-borrow`). Нужен доступ к исходникам базовой конфигурации (`-ConfigPath`) — оттуда читается оригинал метода. `NamePrefix` берётся из `Configuration.xml` расширения.
### Авто-определение ConfigPath
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя
## Параметры
| Параметр | Описание | По умолчанию |
|----------|----------|--------------|
| `ExtensionPath` | Путь к расширению (обязат.) | — |
| `ModulePath` | Путь к модулю (обязат.) | — |
| `MethodName` | Имя перехватываемого метода (обязат.) | — |
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
| `Context` | Директива контекста | `НаСервере` |
| `IsFunction` | Метод — функция (добавит `Возврат`) | false |
| `ConfigPath` | Путь к конфигурации-источнику | обязат., кроме случая, когда `ModulePath` — путь к файлу |
| `ModulePath` | Логическое имя (`Тип.Имя.Модуль`) **или** путь к файлу модуля `.bsl` | обязат. для генерации |
| `MethodName` | Имя перехватываемого метода | обязат. для генерации |
| `InterceptorType` | `Before` / `After` / `Instead` / `ModificationAndControl` | обязат. для генерации |
| `Check` | Проверить контролируемые методы на дрейф (только отчёт) | — |
| `Actualize` | Актуализировать дрейфнувшие контролируемые методы | — |
## Формат ModulePath
@@ -40,39 +49,97 @@ allowed-tools:
Аналогично для Report, DataProcessor, InformationRegister и других типов.
Вместо логического имени в `ModulePath` можно передать **путь к файлу** модуля-источника `.bsl` — тогда `-ConfigPath` указывать не нужно (оригинал читается прямо из этого файла). Путь модуля расширения определяется от типовой папки в пути автоматически.
## Типы перехвата
| InterceptorType | Декоратор | Назначение |
|-----------------|-----------|------------|
| `Before` | `&Перед` | Код до вызова оригинального метода |
| `After` | `&После` | Код после вызова оригинального метода |
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
| InterceptorType | Декоратор | Назначение | Применим к |
|-----------------|-----------|------------|------------|
| `Before` | `&Перед` | Код до вызова оригинального метода | процедуры |
| `After` | `&После` | Код после вызова оригинального метода | процедуры |
| `Instead` | `&Вместо` | Замена метода; в теле — скаффолд `ПродолжитьВызов(...)` | процедуры и функции |
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела оригинала для правки маркерами `#Вставка`/`#Удаление` | процедуры и функции |
## Маркеры `#Вставка` / `#Удаление` (для `ModificationAndControl`)
`&ИзменениеИКонтроль` вставляет в перехватчик **копию тела оригинала**. Дальше отредактируй тело под свою доработку, **помечая каждое изменение** — платформа так отличает твою правку от неизменного оригинала:
- **Добавляешь код** → оберни его `#Вставка``#КонецВставки`.
- **Удаляешь код оригинала** → оберни удаляемые строки `#Удаление``#КонецУдаления`, но сами строки **оставь между маркерами** (платформа сверяет их с оригиналом).
- **Заменяешь** → `#Удаление` старое `#КонецУдаления` сразу за ним `#Вставка` новое `#КонецВставки`.
Пример:
```bsl
&ИзменениеИКонтроль("ПриЗаписи")
Процедура Расш_ПриЗаписи(Отказ)
СуммаДокумента = РассчитатьСумму();
#Вставка
// доработка: округляем
СуммаДокумента = Окр(СуммаДокумента, 2);
#КонецВставки
#Удаление
Записать();
#КонецУдаления
#Вставка
ЗаписатьСПроверкой(Отказ);
#КонецВставки
КонецПроцедуры
```
Правила:
- Маркеры — на **отдельной строке с 0-й колонки** (без отступа), даже внутри отступов и текста запроса (`|…`).
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
- Меняешь только свои `#Вставка`/`#Удаление`; чужой оригинал не трогай.
## Актуализация
После изменения оригинала в конфигурации-источнике перехватчик `&ИзменениеИКонтроль` может рассинхронизироваться — unmarked-контекст разойдётся с новым оригиналом. Платформа при загрузке об этом молчит, поэтому актуализируй сам:
- **Проверить** — `-Check`: отчёт по всем контролируемым методам расширения, ничего не пишет (`exit 1`, если есть дрейф или конфликт).
- **Актуализировать** — `-Actualize`: переносит правки (`#Вставка`/`#Удаление`) на новый оригинал по всему расширению. Сузить область: `-ModulePath` — один модуль, `+ -MethodName` — один метод. Для одного метода то же делает повторный вызов с `-InterceptorType ModificationAndControl`.
Статусы в выводе:
- `[АКТУАЛЕН]` — оригинал не менялся, правок нет;
- `[АКТУАЛИЗИРОВАН]` — тело обновлено по новому оригиналу, правки сохранены (в выводе — сводка);
- `[АКТУАЛИЗИРОВАН-ЧАСТИЧНО]` — часть правок не удалось разместить (якорь изменился в новом оригинале). Они помечены `// [РЕСИНК-КОНФЛИКТ]` в модуле и не потеряны; путь к merge-воркспейсу — в выводе (начни с `index.md`, дальше по каждому конфликту его `conflict.md`, размести блоки вручную в `.bsl` расширения);
- `[ПЕРЕНЕСЕНО В ОСНОВНУЮ]` — правка уже есть в новом оригинале (вставленный код внесён вендором) или удаляемый блок уже вырезан. Правка неактуальна — убирается из тела, дублировать/конфликтовать не нужно. Если так со всеми правками метода — перехватчик можно удалить. `-Check` этим не роняет `exit`.
Повторный вызов `Before`/`After`/`Instead` для уже перехваченного метода дубль не создаёт (`[ПРОПУЩЕН]`).
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
```
## Примеры
```powershell
# Перехват &Перед на сервере
... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
# Код перед записью
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
# Перехват &После на клиенте
... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте"
# Перехват После на форме
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
# ИзменениеИКонтроль для функции
... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction
# Замена функции (ПродолжитьВызов)
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead
# ИзменениеИКонтроль — копия тела для правки маркерами
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl
# ModulePath как путь к файлу модуля-источника (без -ConfigPath)
... -ExtensionPath src\cfe\ИмяРасширения -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead
# Проверить все контролируемые методы расширения на дрейф
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Check
# Актуализировать дрейфнувшие контролируемые методы пачкой
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Actualize
```
## Генерируемый код (Before)
## Верификация
```bsl
&НаСервере
&Перед("ПриЗаписи")
Процедура Расш1_ПриЗаписи()
// TODO: код перед вызовом оригинального метода
КонецПроцедуры
```
/cfe-validate <ExtensionPath>
```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
# cfe-validate v1.4 — Validate 1C configuration extension structure (CFE)
# cfe-validate v1.7 — Validate 1C configuration extension structure (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -108,7 +108,7 @@ $validClassIds = @(
$childObjectTypes = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -122,7 +122,7 @@ $childObjectTypes = @(
# Type -> directory mapping
$childTypeDirMap = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
@@ -197,8 +197,9 @@ if ($root.NamespaceURI -ne $expectedNs) {
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
}
# Must have Configuration child
@@ -930,6 +931,17 @@ if ($script:borrowedFormsWithTree.Count -eq 0) {
Report-OK "13. TypeLink: clean"
}
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
$extRootDir = Split-Path $resolvedPath -Parent
$ctrlCount = 0
foreach ($bslFile in (Get-ChildItem -Path $extRootDir -Recurse -Filter *.bsl -File -ErrorAction SilentlyContinue)) {
$txt = [System.IO.File]::ReadAllText($bslFile.FullName, [System.Text.Encoding]::UTF8)
$ctrlCount += ([regex]::Matches($txt, '(?m)^\s*&ИзменениеИКонтроль\(')).Count
}
if ($ctrlCount -gt 0) {
Out-Line "[INFO] Контролируемых методов (&ИзменениеИКонтроль): $ctrlCount — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>"
}
# --- Final output ---
& $finalize
@@ -1,10 +1,32 @@
#!/usr/bin/env python3
# cfe-validate v1.4 — Validate 1C configuration extension XML structure (CFE)
# cfe-validate v1.7 — Validate 1C configuration extension XML structure (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# 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
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
NS = {
'md': 'http://v8.1c.ru/8.3/MDClasses',
'v8': 'http://v8.1c.ru/8.1/data/core',
@@ -37,7 +59,7 @@ VALID_CLASS_IDS = [
CHILD_OBJECT_TYPES = [
'Language', 'Subsystem', 'StyleItem', 'Style',
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
@@ -54,6 +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',
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
@@ -154,7 +177,7 @@ def main():
parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args()
args = ci_parse_args(parser)
extension_path = args.ExtensionPath
max_errors = args.MaxErrors
@@ -216,8 +239,9 @@ def main():
version = root.get('version', '')
if not version:
r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20', '2.21'):
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'):
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
# Must have Configuration child
cfg_node = None
@@ -885,6 +909,21 @@ def main():
elif check13_ok:
r.ok('13. TypeLink: clean')
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
ctrl_count = 0
for dp, _dn, files in os.walk(config_dir):
for fn in files:
if fn.endswith('.bsl'):
try:
with open(os.path.join(dp, fn), 'r', encoding='utf-8-sig') as f:
for ln in f:
if re.match(r'^\s*&ИзменениеИКонтроль\(', ln):
ctrl_count += 1
except OSError:
pass
if ctrl_count > 0:
r.out('[INFO] Контролируемых методов (&ИзменениеИКонтроль): %d — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>' % ctrl_count)
# --- Final output ---
r.finalize(out_file)
sys.exit(1 if r.errors > 0 else 0)
+2
View File
@@ -45,6 +45,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
| `-AddToList` | нет | Добавить в список баз 1С |
| `-ListName <имя>` | нет | Имя базы в списке |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+247 -20
View File
@@ -1,4 +1,4 @@
# db-create v1.6 — Create 1C information base
# db-create v1.11 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -30,6 +30,12 @@
.PARAMETER ListName
Имя базы в списке
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
@@ -61,12 +67,163 @@ param(
[switch]$AddToList,
[Parameter(Mandatory=$false)]
[string]$ListName
[string]$ListName,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$UseTemplate = ConvertTo-CleanPath $UseTemplate '-UseTemplate'
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -111,35 +268,90 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
function Test-FileIbCreated {
# File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
# Exit code 0 without it (broken/headless env) is a false success — reject it.
param([string]$IbPath)
$f = Join-Path $IbPath "1Cv8.1CD"
return (Test-Path $f) -and ((Get-Item $f -ErrorAction SilentlyContinue).Length -gt 0)
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/UseTemplate' = '-UseTemplate'; '/AddToList' = '-AddToList'; '--db-path' = '-InfoBasePath'; '--load' = '-UseTemplate'; '--restore' = '-UseTemplate' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -173,16 +385,21 @@ try {
}
}
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $((Format-ArgsForDisplay $arguments $engine) -join ' ')"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
$ibMissing = ($exitCode -eq 0) -and -not (Test-FileIbCreated $InfoBasePath)
if ($ibMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
} elseif ($ibMissing) {
Write-Host "Error: exit code 0 but 1Cv8.1CD is missing or empty at $InfoBasePath — information base was not created" -ForegroundColor Red
} else {
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -190,6 +407,8 @@ try {
# --- Build arguments ---
$arguments = @("CREATEINFOBASE")
# Quotes go INSIDE the token (File="path"): 1C's own parser wants them there, quoting
# the whole token instead breaks a path with spaces. Hence -PreQuoted on the launch.
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`""
} else {
@@ -214,19 +433,26 @@ try {
$outFile = Join-Path $tempDir "create_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $((Format-ArgsForDisplay $arguments $engine) -join ' ')"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
# Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
$ibMissing = ($exitCode -eq 0) -and -not ($InfoBaseServer -and $InfoBaseRef) -and -not (Test-FileIbCreated $InfoBasePath)
if ($ibMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
if ($InfoBaseServer -and $InfoBaseRef) {
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
} else {
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
}
} elseif ($ibMissing) {
Write-Host "Error: exit code 0 but 1Cv8.1CD is missing or empty at $InfoBasePath — information base was not created" -ForegroundColor Red
} else {
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
}
@@ -239,6 +465,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+323 -25
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-create v1.6 — Create 1C information base
# db-create v1.11 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys
import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -36,6 +58,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -78,6 +257,13 @@ def resolve_v8path(v8path):
return v8path
def file_ib_created(ib_path):
"""File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
Exit code 0 without it (broken/headless env) is a false success reject it."""
f = os.path.join(ib_path, "1Cv8.1CD")
return os.path.isfile(f) and os.path.getsize(f) > 0
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
@@ -86,6 +272,18 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +294,67 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def main():
@@ -113,11 +371,33 @@ def main():
parser.add_argument("-UseTemplate", default="")
parser.add_argument("-AddToList", action="store_true")
parser.add_argument("-ListName", default="")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
args.UseTemplate = clean_path(args.UseTemplate, "-UseTemplate")
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/UseTemplate": "-UseTemplate",
"/AddToList": "-AddToList",
"--db-path": "-InfoBasePath",
"--load": "-UseTemplate",
"--restore": "-UseTemplate",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -143,17 +423,25 @@ def main():
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(quote_if_needed(a) for a in extra_args)
print(f"Running: ibcmd {' '.join(format_args_for_display(arguments, engine))}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
if result.returncode == 0:
exit_code = result.returncode
ib_missing = exit_code == 0 and not file_ib_created(args.InfoBasePath)
if ib_missing:
exit_code = 1
if exit_code == 0:
print(f"Information base created successfully: {args.InfoBasePath}")
elif ib_missing:
print(
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
"— information base was not created",
file=sys.stderr,
)
else:
print(f"Error creating information base (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
print_platform_output(result)
sys.exit(exit_code)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
@@ -163,44 +451,53 @@ def main():
# --- Build arguments ---
arguments = ["CREATEINFOBASE"]
# Quotes go INSIDE the token (File="path"): that is where 1C's parser expects them.
# Quoting the whole token instead breaks a path with spaces — on both OSes.
if args.InfoBaseServer and args.InfoBaseRef:
# No embedded quotes: subprocess quotes the whole token; 1C's argv parser
# strips outer quotes. Inner quotes get escaped by list2cmdline and break parsing.
arguments.append(f'Srvr={args.InfoBaseServer};Ref={args.InfoBaseRef}')
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
else:
arguments.append(f'File={args.InfoBasePath}')
arguments.append(f'File="{args.InfoBasePath}"')
# --- Template ---
if args.UseTemplate:
arguments.extend(["/UseTemplate", args.UseTemplate])
arguments.extend(["/UseTemplate", f'"{args.UseTemplate}"'])
# --- Add to list ---
if args.AddToList:
if args.ListName:
arguments.extend(["/AddToList", args.ListName])
arguments.extend(["/AddToList", f'"{args.ListName}"'])
else:
arguments.append("/AddToList")
# --- Output ---
out_file = os.path.join(temp_dir, "create_log.txt")
arguments.extend(["/Out", out_file])
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {' '.join(format_args_for_display(arguments, engine))}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
# Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
is_server = bool(args.InfoBaseServer and args.InfoBaseRef)
ib_missing = exit_code == 0 and not is_server and not file_ib_created(args.InfoBasePath)
if ib_missing:
exit_code = 1
if exit_code == 0:
if args.InfoBaseServer and args.InfoBaseRef:
if is_server:
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
else:
print(f"Information base created successfully: {args.InfoBasePath}")
elif ib_missing:
print(
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
"— information base was not created",
file=sys.stderr,
)
else:
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
@@ -214,6 +511,7 @@ def main():
print("--- End ---")
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
+2
View File
@@ -51,6 +51,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
| `-OutputFile <путь>` | да | Путь к выходному CF-файлу |
| `-Extension <имя>` | нет | Выгрузить расширение |
| `-AllExtensions` | нет | Выгрузить все расширения |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+263 -20
View File
@@ -1,4 +1,4 @@
# db-dump-cf v1.6 — Dump 1C configuration to CF file
# db-dump-cf v1.13 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -36,6 +36,12 @@
.PARAMETER AllExtensions
Выгрузить все расширения
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
@@ -70,12 +76,183 @@ param(
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$OutputFile = ConvertTo-CleanPath $OutputFile '-OutputFile'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -120,35 +297,89 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
function Test-OutputNonEmpty {
# Postcondition: the platform must have produced a non-empty output file.
# Exit code 0 without it (broken/headless env) is a false success — reject it.
param([string]$Path)
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -183,16 +414,21 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — configuration was not dumped" -ForegroundColor Red
} else {
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -222,15 +458,21 @@ try {
$outFile = Join-Path $tempDir "dump_cf_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — configuration was not dumped" -ForegroundColor Red
} else {
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
}
@@ -243,6 +485,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+330 -24
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-dump-cf v1.6 — Dump 1C configuration to CF file
# db-dump-cf v1.13 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys
import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -36,6 +58,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +354,25 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def output_nonempty(path):
"""Postcondition: the platform must have produced a non-empty output file.
Exit code 0 without it (broken/headless env) is a false success reject it."""
return os.path.isfile(path) and os.path.getsize(path) > 0
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -115,11 +391,34 @@ def main():
parser.add_argument("-OutputFile", required=True)
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -150,17 +449,20 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
exit_code = result.returncode
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Configuration dumped successfully to: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
else:
print(f"Error dumping configuration (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
@@ -171,40 +473,43 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", args.InfoBasePath])
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments.extend(["/DumpCfg", args.OutputFile])
arguments.extend(["/DumpCfg", f'"{args.OutputFile}"'])
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", args.Extension])
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
arguments.extend(["/Out", out_file])
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Configuration dumped successfully to: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
@@ -219,6 +524,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+2
View File
@@ -52,6 +52,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+264 -20
View File
@@ -1,4 +1,4 @@
# db-dump-dt v1.5 — Dump 1C information base to DT file
# db-dump-dt v1.12 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -29,6 +29,12 @@
.PARAMETER OutputFile
Путь к выходному DT-файлу
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
#>
@@ -54,12 +60,183 @@ param(
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$OutputFile
[string]$OutputFile,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$OutputFile = ConvertTo-CleanPath $OutputFile '-OutputFile'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -104,35 +281,89 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
function Test-OutputNonEmpty {
# Postcondition: the platform must have produced a non-empty output file.
# Exit code 0 without it (broken/headless env) is a false success — reject it.
param([string]$Path)
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -163,16 +394,22 @@ try {
$arguments += "$OutputFile"
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — information base was not dumped" -ForegroundColor Red
} else {
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -195,15 +432,21 @@ try {
$outFile = Join-Path $tempDir "dump_dt_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — information base was not dumped" -ForegroundColor Red
} else {
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
}
@@ -216,6 +459,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+329 -23
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-dump-dt v1.5 — Dump 1C information base to DT file
# db-dump-dt v1.12 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys
import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -36,6 +58,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +354,25 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def output_nonempty(path):
"""Postcondition: the platform must have produced a non-empty output file.
Exit code 0 without it (broken/headless env) is a false success reject it."""
return os.path.isfile(path) and os.path.getsize(path) > 0
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -113,11 +389,34 @@ def main():
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-OutputFile", required=True)
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -143,17 +442,20 @@ def main():
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
exit_code = result.returncode
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Information base dumped successfully to: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
else:
print(f"Error dumping information base (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
@@ -164,34 +466,37 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", args.InfoBasePath])
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments.extend(["/DumpIB", args.OutputFile])
arguments.extend(["/DumpIB", f'"{args.OutputFile}"'])
# --- Output ---
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
arguments.extend(["/Out", out_file])
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Information base dumped successfully to: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
else:
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
@@ -206,6 +511,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+2
View File
@@ -56,6 +56,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
| `-Extension <имя>` | нет | Выгрузить расширение |
| `-AllExtensions` | нет | Выгрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
@@ -1,4 +1,4 @@
# db-dump-xml v1.8 — Dump 1C configuration to XML files
# db-dump-xml v1.15 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -48,6 +48,12 @@
.PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
@@ -93,12 +99,183 @@ param(
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical"
[string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$ConfigDir = ConvertTo-CleanPath $ConfigDir '-ConfigDir'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -143,35 +320,89 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
function Test-DirNonEmpty {
# Postcondition: the platform must have written files into the output directory.
# Exit code 0 with an empty dir (broken/headless env) is a false success — reject it.
param([string]$Path)
return (Test-Path $Path -PathType Container) -and ([bool](Get-ChildItem -LiteralPath $Path -Force -ErrorAction SilentlyContinue | Select-Object -First 1))
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -224,16 +455,21 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no files under $ConfigDir — configuration was not exported" -ForegroundColor Red
} else {
Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -291,16 +527,22 @@ try {
$outFile = Join-Path $tempDir "dump_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
# Postcondition: exit 0 with an empty output directory is a false success.
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Dump completed successfully" -ForegroundColor Green
Write-Host "Configuration dumped to: $ConfigDir"
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no files under $ConfigDir — configuration was not dumped" -ForegroundColor Red
} else {
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
}
@@ -313,6 +555,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+331 -25
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-dump-xml v1.8 — Dump 1C configuration to XML files
# db-dump-xml v1.15 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys
import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -36,6 +58,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +354,25 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def dir_nonempty(path):
"""Postcondition: the platform must have written files into the output directory.
Exit code 0 with an empty dir (broken/headless env) is a false success reject it."""
return os.path.isdir(path) and any(os.scandir(path))
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -128,12 +404,35 @@ def main():
choices=["Hierarchical", "Plain"],
help="Dump format (default: Hierarchical)",
)
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir")
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -181,17 +480,20 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
exit_code = result.returncode
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Configuration exported successfully to: {args.ConfigDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported", file=sys.stderr)
else:
print(f"Error exporting configuration (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
@@ -202,16 +504,16 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", args.InfoBasePath]
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments += ["/DumpConfigToFiles", args.ConfigDir]
arguments += ["/DumpConfigToFiles", f'"{args.ConfigDir}"']
arguments += ["-Format", args.Format]
if args.Mode == "Full":
@@ -228,7 +530,7 @@ def main():
with open(list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(object_list))
arguments += ["-listFile", list_file]
arguments += ["-listFile", f'"{list_file}"']
print(f"Objects to dump: {len(object_list)}")
for obj in object_list:
print(f" {obj}")
@@ -238,28 +540,31 @@ def main():
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", args.Extension]
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "dump_log.txt")
arguments += ["/Out", out_file]
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
# Postcondition: exit 0 with an empty output directory is a false success.
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
if out_missing:
exit_code = 1
if exit_code == 0:
print("Dump completed successfully")
print(f"Configuration dumped to: {args.ConfigDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped", file=sys.stderr)
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
@@ -274,6 +579,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+3
View File
@@ -29,6 +29,7 @@ allowed-tools:
```json
{
"v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin",
"v8args": ["/UseHwLicenses+"],
"databases": [
{
"id": "dev",
@@ -61,6 +62,8 @@ allowed-tools:
| Поле | Тип | Описание |
|------|-----|----------|
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
| `v8args` | array | Доп. аргументы запуска `1cv8.exe` для всех навыков, напр. `["/UseHwLicenses+"]` |
| `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) |
| `databases` | array | Массив баз данных |
| `default` | string | id базы по умолчанию |
+2
View File
@@ -52,6 +52,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
| `-InputFile <путь>` | да | Путь к CF-файлу |
| `-Extension <имя>` | нет | Загрузить как расширение |
| `-AllExtensions` | нет | Загрузить все расширения из архива |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+266 -22
View File
@@ -1,4 +1,4 @@
# db-load-cf v1.6 — Load 1C configuration from CF file
# db-load-cf v1.14 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -36,6 +36,12 @@
.PARAMETER AllExtensions
Загрузить все расширения из архива
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
@@ -70,12 +76,200 @@ param(
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
function Get-ExitAnnotation {
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
# half-updated — surface that instead of a plain code. (Windows exception codes only;
# POSIX signals are handled in the .py port.)
param([int]$Code)
$win = @{
-1073741819 = "0xC0000005 (access violation)"
-1073741515 = "0xC0000135 (missing DLL)"
-1073740791 = "0xC0000409 (stack overrun)"
}
if ($win.ContainsKey($Code)) {
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
}
return ""
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$InputFile = ConvertTo-CleanPath $InputFile '-InputFile'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -120,35 +314,82 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -183,16 +424,17 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -222,17 +464,18 @@ try {
$outFile = Join-Path $tempDir "load_cf_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
if ($exitCode -eq 0) {
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
@@ -243,6 +486,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+335 -23
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-cf v1.6 — Load 1C configuration from CF file
# db-load-cf v1.14 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys
import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -36,6 +58,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +354,43 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -115,11 +409,34 @@ def main():
parser.add_argument("-InputFile", required=True)
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.InputFile = clean_path(args.InputFile, "-InputFile")
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -150,16 +467,13 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
print(f"Configuration loaded successfully from: {args.InputFile}")
else:
print(f"Error loading configuration (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir ---
@@ -171,42 +485,39 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", args.InfoBasePath])
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments.extend(["/LoadCfg", args.InputFile])
arguments.extend(["/LoadCfg", f'"{args.InputFile}"'])
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", args.Extension])
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "load_cf_log.txt")
arguments.extend(["/Out", out_file])
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Configuration loaded successfully from: {args.InputFile}")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if os.path.isfile(out_file):
try:
@@ -219,6 +530,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+2
View File
@@ -68,6 +68,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п
| `-InputFile <путь>` | да | Путь к DT-файлу |
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+267 -22
View File
@@ -1,4 +1,4 @@
# db-load-dt v1.5 — Load 1C information base from DT file
# db-load-dt v1.13 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -36,6 +36,12 @@
.PARAMETER UnlockCode
Код разблокировки базы (/UC) если заблокировано начало сеансов
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
#>
@@ -67,12 +73,200 @@ param(
[int]$JobsCount = 0,
[Parameter(Mandatory=$false)]
[string]$UnlockCode
[string]$UnlockCode,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
function Get-ExitAnnotation {
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
# half-updated — surface that instead of a plain code. (Windows exception codes only;
# POSIX signals are handled in the .py port.)
param([int]$Code)
$win = @{
-1073741819 = "0xC0000005 (access violation)"
-1073741515 = "0xC0000135 (missing DLL)"
-1073740791 = "0xC0000409 (stack overrun)"
}
if ($win.ContainsKey($Code)) {
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
}
return ""
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$InputFile = ConvertTo-CleanPath $InputFile '-InputFile'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -117,35 +311,82 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -177,16 +418,18 @@ try {
$arguments += "$InputFile"
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -211,17 +454,18 @@ try {
$outFile = Join-Path $tempDir "load_dt_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
if ($exitCode -eq 0) {
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
@@ -232,6 +476,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+335 -23
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-dt v1.5 — Load 1C information base from DT file
# db-load-dt v1.13 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys
import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -36,6 +58,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +354,43 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -115,11 +409,34 @@ def main():
parser.add_argument("-InputFile", required=True)
parser.add_argument("-JobsCount", type=int, default=0)
parser.add_argument("-UnlockCode", default="")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.InputFile = clean_path(args.InputFile, "-InputFile")
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -147,16 +464,13 @@ def main():
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
print(f"Information base restored successfully from: {args.InputFile}")
else:
print(f"Error restoring information base (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir ---
@@ -168,40 +482,37 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", args.InfoBasePath])
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
if args.UnlockCode:
arguments.append(f"/UC{args.UnlockCode}")
arguments.append(f'/UC"{args.UnlockCode}"')
arguments.extend(["/RestoreIB", args.InputFile])
arguments.extend(["/RestoreIB", f'"{args.InputFile}"'])
if args.JobsCount > 0:
arguments.extend(["-JobsCount", str(args.JobsCount)])
# --- Output ---
out_file = os.path.join(temp_dir, "load_dt_log.txt")
arguments.extend(["/Out", out_file])
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Information base restored successfully from: {args.InputFile}")
else:
print(f"Error restoring information base (code: {exit_code})", file=sys.stderr)
print(f"Error restoring information base (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if os.path.isfile(out_file):
try:
@@ -214,6 +525,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+2
View File
@@ -59,6 +59,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
@@ -1,4 +1,4 @@
# db-load-git v1.11 — Load Git changes into 1C database
# db-load-git v1.19 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -48,6 +48,12 @@
.PARAMETER DryRun
Только показать что будет загружено (без загрузки)
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
@@ -102,12 +108,164 @@ param(
[switch]$DryRun,
[Parameter(Mandatory=$false)]
[switch]$UpdateDB
[switch]$UpdateDB,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function Get-ExitAnnotation {
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
# half-updated — surface that instead of a plain code. (Windows exception codes only;
# POSIX signals are handled in the .py port.)
param([int]$Code)
$win = @{
-1073741819 = "0xC0000005 (access violation)"
-1073741515 = "0xC0000135 (missing DLL)"
-1073740791 = "0xC0000409 (stack overrun)"
}
if ($win.ContainsKey($Code)) {
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
}
return ""
}
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
function Get-ObjectXmlFromSubFile {
param([string]$RelativePath)
@@ -167,32 +325,75 @@ if (-not $DryRun) {
# --- Detect engine + validate connection (skip if DryRun) ---
$engine = "1cv8"
if (-not $DryRun) {
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($engine -eq "ibcmd") {
@@ -206,6 +407,10 @@ function Invoke-IbcmdProcess {
}
}
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate config dir ---
if (-not (Test-Path $ConfigDir)) {
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
@@ -372,32 +577,34 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -ne 0) {
Write-Host "Error loading changes (code: $exitCode)" -ForegroundColor Red
if ($output) { Write-Host ($output | Out-String) }
Write-Host "Error loading changes (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
Write-PlatformOutput $output
exit $exitCode
}
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
if ($UpdateDB) {
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
if ($UserName) { $applyArgs += "--user=$UserName" }
if ($Password) { $applyArgs += "--password=$Password" }
$applyArgs += "--data=$tempDir"
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
$applyArgs += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $applyArgs $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $applyArgs
$applyOut = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($applyOut) { Write-Host ($applyOut | Out-String) }
Write-PlatformOutput $applyOut
}
exit $exitCode
}
@@ -442,21 +649,22 @@ try {
$outFile = Join-Path $tempDir "load_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host ""
Write-Host "Executing partial configuration load..."
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
Write-Host ""
if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
@@ -467,6 +675,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+340 -32
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-git v1.11 — Load Git changes into 1C database
# db-load-git v1.19 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys
import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -36,6 +58,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +354,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def get_object_xml_from_subfile(relative_path):
@@ -121,6 +382,39 @@ def run_git(config_dir, git_args):
return []
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -152,7 +446,18 @@ def main():
)
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir")
# --- Resolve V8Path (skip if DryRun) ---
v8path = None
@@ -171,6 +476,18 @@ def main():
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate config dir ---
if not os.path.exists(args.ConfigDir):
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
@@ -307,18 +624,13 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode != 0:
print(f"Error loading changes (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
sys.exit(result.returncode)
print(f"Changes loaded successfully ({len(config_files)} files)")
if result.stdout:
print(result.stdout)
exit_code = 0
if args.UpdateDB:
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
@@ -327,17 +639,15 @@ def main():
if args.Password:
apply_args.append(f"--password={args.Password}")
apply_args.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(apply_args)}")
apply_args.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(apply_args, engine)), args.Password, args.UserName)}")
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
exit_code = ar.returncode
if exit_code == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
if ar.stdout:
print(ar.stdout)
if ar.stderr:
print(ar.stderr, file=sys.stderr)
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
print_platform_output(ar)
sys.exit(exit_code)
# --- Write list file (UTF-8 with BOM) ---
@@ -349,24 +659,24 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", args.InfoBasePath]
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadConfigFromFiles", args.ConfigDir]
arguments += ["-listFile", list_file]
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
arguments += ["-listFile", f'"{list_file}"']
arguments += ["-Format", args.Format]
arguments.append("-partial")
arguments.append("-updateConfigDumpInfo")
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", args.Extension]
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
@@ -376,19 +686,16 @@ def main():
# --- Output ---
out_file = os.path.join(temp_dir, "load_log.txt")
arguments += ["/Out", out_file]
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print("")
print("Executing partial configuration load...")
print(f"Running: 1cv8.exe {' '.join(arguments)}")
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
@@ -396,7 +703,7 @@ def main():
if exit_code == 0:
print("Load completed successfully")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if os.path.isfile(out_file):
try:
@@ -409,6 +716,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+2
View File
@@ -59,6 +59,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <
| `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
@@ -1,4 +1,4 @@
# db-load-xml v1.12 — Load 1C configuration from XML files
# db-load-xml v1.20 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -48,6 +48,12 @@
.PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
@@ -102,12 +108,201 @@ param(
[switch]$UpdateDB,
[Parameter(Mandatory=$false)]
[switch]$StrictLog
[switch]$StrictLog,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
function Get-ExitAnnotation {
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
# half-updated — surface that instead of a plain code. (Windows exception codes only;
# POSIX signals are handled in the .py port.)
param([int]$Code)
$win = @{
-1073741819 = "0xC0000005 (access violation)"
-1073741515 = "0xC0000135 (missing DLL)"
-1073740791 = "0xC0000409 (stack overrun)"
}
if ($win.ContainsKey($Code)) {
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
}
return ""
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$ConfigDir = ConvertTo-CleanPath $ConfigDir '-ConfigDir'
$ListFile = ConvertTo-CleanPath $ListFile '-ListFile'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -152,35 +347,82 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -244,33 +486,35 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -ne 0) {
Write-Host "Error loading configuration from files (code: $exitCode)" -ForegroundColor Red
if ($output) { Write-Host ($output | Out-String) }
Write-Host "Error loading configuration from files (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
Write-PlatformOutput $output
exit $exitCode
}
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
if ($UpdateDB) {
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
if ($UserName) { $applyArgs += "--user=$UserName" }
if ($Password) { $applyArgs += "--password=$Password" }
$applyArgs += "--data=$tempDir"
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
$applyArgs += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $applyArgs $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $applyArgs
$applyOut = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($applyOut) { Write-Host ($applyOut | Out-String) }
Write-PlatformOutput $applyOut
}
exit $exitCode
}
@@ -349,11 +593,12 @@ try {
$outFile = Join-Path $tempDir "load_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Read log ---
$logContent = $null
@@ -392,7 +637,7 @@ try {
if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($logContent) {
@@ -400,6 +645,7 @@ try {
Write-Host $logContent
Write-Host "--- End ---"
}
Write-PlatformOutput $__v8.Output
if ($silentFailures.Count -gt 0) {
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
+341 -32
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-xml v1.12 — Load 1C configuration from XML files
# db-load-xml v1.20 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys
import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -36,6 +58,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +354,43 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -135,13 +429,37 @@ def main():
action="store_true",
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
)
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir")
args.ListFile = clean_path(args.ListFile, "-ListFile")
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -199,18 +517,13 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode != 0:
print(f"Error loading configuration from files (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
sys.exit(result.returncode)
print(f"Configuration loaded successfully from: {args.ConfigDir}")
if result.stdout:
print(result.stdout)
exit_code = 0
if args.UpdateDB:
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
@@ -219,17 +532,15 @@ def main():
if args.Password:
apply_args.append(f"--password={args.Password}")
apply_args.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(apply_args)}")
apply_args.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(apply_args, engine)), args.Password, args.UserName)}")
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
exit_code = ar.returncode
if exit_code == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
if ar.stdout:
print(ar.stdout)
if ar.stderr:
print(ar.stderr, file=sys.stderr)
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
print_platform_output(ar)
sys.exit(exit_code)
# --- Temp dir ---
@@ -241,16 +552,16 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", args.InfoBasePath]
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadConfigFromFiles", args.ConfigDir]
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
if args.Mode == "Full":
print("Executing full configuration load...")
@@ -286,7 +597,7 @@ def main():
for fl in file_list:
print(f" {fl}")
arguments += ["-listFile", generated_list_file]
arguments += ["-listFile", f'"{generated_list_file}"']
arguments.append("-partial")
arguments.append("-updateConfigDumpInfo")
@@ -294,7 +605,7 @@ def main():
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", args.Extension]
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
@@ -304,16 +615,13 @@ def main():
# --- Output ---
out_file = os.path.join(temp_dir, "load_log.txt")
arguments += ["/Out", out_file]
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Read log ---
@@ -352,13 +660,14 @@ def main():
if exit_code == 0:
print("Load completed successfully")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
print_platform_output(result)
if silent_failures:
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
print(
+1
View File
@@ -52,6 +52,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <пар
| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта |
| `-CParam <строка>` | нет | Параметр запуска (/C) |
| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+203 -5
View File
@@ -1,4 +1,4 @@
# db-run v1.2 — Launch 1C:Enterprise
# db-run v1.8 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -36,6 +36,12 @@
.PARAMETER URL
Навигационная ссылка (e1cib/...)
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
@@ -73,12 +79,170 @@ param(
[string]$CParam,
[Parameter(Mandatory=$false)]
[string]$URL
[string]$URL,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$Execute = ConvertTo-CleanPath $Execute '-Execute'
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -122,6 +286,19 @@ if (-not (Test-Path $V8Path)) {
exit 1
}
# --- Resolve additional arguments ---
# 1C:Enterprise is always launched by 1cv8 — ibcmd has no interactive mode.
$engine = "1cv8"
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '/Execute' = '-Execute'; '/C' = '-CParam'; '/URL' = '-URL' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
function Format-ArgToken {
# ShellExecute re-joins the argument string, so quote each extra token that needs it.
param([string]$Token)
if ($Token -match '[\s"]') { return ' "' + ($Token -replace '"', '\"') + '"' }
return " $Token"
}
# --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
@@ -165,7 +342,28 @@ if ($URL) {
$argString += " /DisableStartupDialogs"
# --- Execute (background, no wait) ---
Write-Host "Running: 1cv8.exe $argString"
Start-Process -FilePath $V8Path -ArgumentList $argString
# The display string is built from the same tokens with secret-prone values redacted.
$displayString = $argString
foreach ($tok in $extraArgs) { $argString += (Format-ArgToken $tok) }
foreach ($tok in (Format-ArgsForDisplay $extraArgs $engine)) { $displayString += (Format-ArgToken $tok) }
# --- Execute (background) ---
# Redact the password/user before printing the command line — never leak secrets.
$displayArg = Protect-Secrets $displayString @($Password, $UserName)
Write-Host "Running: 1cv8.exe $displayArg"
$proc = Start-Process -FilePath $V8Path -ArgumentList $argString -PassThru
# --- Bounded early-exit check ---
# The launch is a background GUI process, so we don't wait for completion. But a process
# that dies within the first ~1.5s never really started (bad base, no display, license) —
# report that honestly instead of a blind "launched".
$deadline = (Get-Date).AddMilliseconds(1500)
while ((Get-Date) -lt $deadline -and -not $proc.HasExited) {
Start-Sleep -Milliseconds 200
}
if ($proc.HasExited) {
Write-Host "Error: 1C:Enterprise exited immediately (code: $($proc.ExitCode))" -ForegroundColor Red
if ($proc.ExitCode -ne 0) { exit $proc.ExitCode } else { exit 1 }
}
Write-Host "PID: $($proc.Id)"
Write-Host "1C:Enterprise launched" -ForegroundColor Green
+251 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-run v1.2 — Launch 1C:Enterprise
# db-run v1.8 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -9,6 +9,29 @@ import os
import re
import subprocess
import sys
import time
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path():
@@ -32,6 +55,181 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -74,6 +272,15 @@ def resolve_v8path(v8path):
return v8path
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -90,10 +297,34 @@ def main():
parser.add_argument("-Execute", default="")
parser.add_argument("-CParam", default="")
parser.add_argument("-URL", default="")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
args.Execute = clean_path(args.Execute, "-Execute")
v8path = resolve_v8path(args.V8Path)
# --- Resolve additional arguments ---
# 1C:Enterprise is always launched by 1cv8 — ibcmd has no interactive mode.
engine = "1cv8"
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"/Execute": "-Execute",
"/C": "-CParam",
"/URL": "-URL",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
@@ -130,10 +361,25 @@ def main():
arguments.extend(["/URL", args.URL])
arguments.append("/DisableStartupDialogs")
arguments.extend(extra_args)
# --- Execute (background, no wait) ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
subprocess.Popen([v8path] + arguments)
# --- Execute (background) ---
# Redact the password/user before printing the command line — never leak secrets.
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
proc = subprocess.Popen([v8path] + arguments)
# --- Bounded early-exit check ---
# The launch is a background GUI process, so we don't wait for completion. But a process
# that dies within the first ~1.5s never really started (bad base, no display, license) —
# report that honestly instead of a blind "launched".
deadline = time.monotonic() + 1.5
while time.monotonic() < deadline and proc.poll() is None:
time.sleep(0.2)
rc = proc.poll()
if rc is not None:
print(f"Error: 1C:Enterprise exited immediately (code: {rc})", file=sys.stderr)
sys.exit(rc if rc and rc > 0 else 1)
print(f"PID: {proc.pid}")
print("1C:Enterprise launched")
+2
View File
@@ -53,6 +53,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
| `-Server` | нет | Обновление на стороне сервера |
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+265 -22
View File
@@ -1,4 +1,4 @@
# db-update v1.6 — Update 1C database configuration
# db-update v1.14 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -42,6 +42,12 @@
.PARAMETER WarningsAsErrors
Предупреждения считать ошибками
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
@@ -83,12 +89,199 @@ param(
[switch]$Server,
[Parameter(Mandatory=$false)]
[switch]$WarningsAsErrors
[switch]$WarningsAsErrors,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
function Get-ExitAnnotation {
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
# half-updated — surface that instead of a plain code. (Windows exception codes only;
# POSIX signals are handled in the .py port.)
param([int]$Code)
$win = @{
-1073741819 = "0xC0000005 (access violation)"
-1073741515 = "0xC0000135 (missing DLL)"
-1073740791 = "0xC0000409 (stack overrun)"
}
if ($win.ContainsKey($Code)) {
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
}
return ""
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -133,35 +326,82 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -191,16 +431,17 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -241,17 +482,18 @@ try {
$outFile = Join-Path $tempDir "update_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
@@ -262,6 +504,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+333 -22
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-update v1.6 — Update 1C database configuration
# db-update v1.14 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys
import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -36,6 +58,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +354,43 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -117,12 +411,34 @@ def main():
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
parser.add_argument("-Server", action="store_true")
parser.add_argument("-WarningsAsErrors", action="store_true")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -151,16 +467,13 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir ---
@@ -172,14 +485,14 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", args.InfoBasePath])
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments.append("/UpdateDBCfg")
@@ -193,29 +506,26 @@ def main():
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", args.Extension])
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "update_log.txt")
arguments.extend(["/Out", out_file])
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if os.path.isfile(out_file):
try:
@@ -228,6 +538,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+2
View File
@@ -55,6 +55,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
| `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
+278 -22
View File
@@ -1,4 +1,4 @@
# epf-build v1.6 — Build external data processor or report (EPF/ERF) from XML sources
# epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -33,6 +33,12 @@
.PARAMETER OutputFile
Путь к выходному EPF/ERF-файлу
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
@@ -64,12 +70,184 @@ param(
[string]$SourceFile,
[Parameter(Mandatory=$true)]
[string]$OutputFile
[string]$OutputFile,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$SourceFile = ConvertTo-CleanPath $SourceFile '-SourceFile'
$OutputFile = ConvertTo-CleanPath $OutputFile '-OutputFile'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -114,34 +292,88 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
function Test-OutputNonEmpty {
# Postcondition: the platform must have produced a non-empty output file.
# Exit code 0 without it (broken/headless env) is a false success — reject it.
param([string]$Path)
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red
exit 1
@@ -154,8 +386,20 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
Write-Host "No database specified. Creating temporary stub database..."
$stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`""
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru
# The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles,
# UpdateDBCfg) — they need the same extra arguments as the final build. Only the
# explicit ones are forwarded: the stub reads .v8-project.json itself.
# Invoked via -Command, not -File: -File takes the tail literally, so an array
# parameter would arrive as a single comma-glued token.
$q = { param($s) "'" + ($s -replace "'", "''") + "'" }
$stubCmd = "& $(& $q $stubScript) -SourceDir $(& $q $sourceDir) -V8Path $(& $q $V8Path) -TempBasePath $(& $q $autoBasePath)"
if ($AdditionalV8Arguments.Count -gt 0) {
$stubCmd += " -AdditionalV8Arguments " + (($AdditionalV8Arguments | ForEach-Object { & $q $_ }) -join ',')
}
if ($AdditionalIbcmdArguments.Count -gt 0) {
$stubCmd += " -AdditionalIbcmdArguments " + (($AdditionalIbcmdArguments | ForEach-Object { & $q $_ }) -join ',')
}
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -Command `"$stubCmd`"" -NoNewWindow -Wait -PassThru
if ($stubProc.ExitCode -ne 0) {
Write-Host "Error: failed to create stub database" -ForegroundColor Red
exit 1
@@ -188,16 +432,21 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "External data processor/report built successfully: $OutputFile" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — build produced no output" -ForegroundColor Red
} else {
Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -220,15 +469,21 @@ try {
$outFile = Join-Path $tempDir "build_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — build produced no output" -ForegroundColor Red
} else {
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red
}
@@ -241,6 +496,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+340 -27
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# epf-build v1.6 — Build external data processor or report (EPF/ERF) from XML sources
# epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys
import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -36,6 +58,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +354,25 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def output_nonempty(path):
"""Postcondition: the platform must have produced a non-empty output file.
Exit code 0 without it (broken/headless env) is a false success reject it."""
return os.path.isfile(path) and os.path.getsize(path) > 0
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -114,11 +390,35 @@ def main():
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.SourceFile = clean_path(args.SourceFile, "-SourceFile")
args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
if engine == "ibcmd" and args.InfoBaseServer and args.InfoBaseRef:
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)", file=sys.stderr)
sys.exit(1)
@@ -130,10 +430,16 @@ def main():
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
print("No database specified. Creating temporary stub database...")
result = subprocess.run(
[sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path, "-TempBasePath", auto_base_path],
capture_output=False,
)
stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path,
"-TempBasePath", auto_base_path]
# The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles,
# UpdateDBCfg) — they need the same extra arguments as the final build. Only the
# explicit ones are forwarded: the stub reads .v8-project.json itself.
if v8_extra:
stub_cmd += ["-AdditionalV8Arguments"] + list(v8_extra)
if ibcmd_extra:
stub_cmd += ["-AdditionalIbcmdArguments"] + list(ibcmd_extra)
result = subprocess.run(stub_cmd, capture_output=False)
if result.returncode != 0:
print("Error: failed to create stub database", file=sys.stderr)
sys.exit(1)
@@ -166,50 +472,56 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
if result.returncode == 0:
exit_code = result.returncode
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"External data processor/report built successfully: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
else:
print(f"Error building external data processor/report (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", args.InfoBasePath]
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadExternalDataProcessorOrReportFromFiles", args.SourceFile, args.OutputFile]
arguments += ["/LoadExternalDataProcessorOrReportFromFiles", f'"{args.SourceFile}"', f'"{args.OutputFile}"']
# --- Output ---
out_file = os.path.join(temp_dir, "build_log.txt")
arguments += ["/Out", out_file]
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Build completed successfully: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
else:
print(f"Error building (code: {exit_code})", file=sys.stderr)
@@ -224,6 +536,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
@@ -1,4 +1,4 @@
# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -7,12 +7,162 @@ param(
[Parameter(Mandatory)]
[string]$V8Path,
[string]$TempBasePath
[string]$TempBasePath,
[string[]]$AdditionalV8Arguments = @(),
[string[]]$AdditionalIbcmdArguments = @()
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$SourceDir = ConvertTo-CleanPath $SourceDir '-SourceDir'
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$TempBasePath = ConvertTo-CleanPath $TempBasePath '-TempBasePath'
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
# --- 1. Scan XML files for reference types ---
$typeMap = @{} # MetadataType -> @(Name1, Name2, ...)
@@ -1253,34 +1403,89 @@ $propsXml </Properties>$childObjLine
}
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
$stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-TempBasePath'; '--db-path' = '-TempBasePath' }
$extraArgs = @(Resolve-ExtraArgs $stubEngine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
function Format-ArgToken {
# Start-Process takes these argument lists as one string, so quote each token that needs it.
param([string]$Token)
if ($Token -match '[\s"]') { return ' "' + ($Token -replace '"', '\"') + '"' }
return " $Token"
}
$extraArgString = -join ($extraArgs | ForEach-Object { Format-ArgToken $_ })
if ($stubEngine -eq "ibcmd") {
Write-Host "Creating infobase (ibcmd): $TempBasePath"
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
@@ -1288,12 +1493,13 @@ if ($stubEngine -eq "ibcmd") {
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
$ibArgs += "--data=$ibData"
$__ib = Invoke-IbcmdProcess $V8Path $ibArgs
$ibArgs += $extraArgs
$__ib = Invoke-PlatformProcess $V8Path $ibArgs
$ibOut = $__ib.Output
$ibRc = $__ib.ExitCode
Remove-Item -Path $ibData -Recurse -Force -ErrorAction SilentlyContinue
if ($ibRc -ne 0) {
if ($ibOut) { Write-Host ($ibOut | Out-String) }
Write-PlatformOutput $ibOut
Write-Error "Failed to create stub infobase (code: $ibRc)"
exit 1
}
@@ -1305,9 +1511,10 @@ if ($stubEngine -eq "ibcmd") {
# --- 5. Create infobase ---
Write-Host "Creating infobase: $TempBasePath"
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
$proc = Start-Process -FilePath $V8Path -ArgumentList $createArgs -NoNewWindow -Wait -PassThru
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs" + $extraArgString
$proc = Invoke-PlatformProcess $V8Path @($createArgs) -PreQuoted
if ($proc.ExitCode -ne 0) {
Write-PlatformOutput $proc.Output
Write-Error "Failed to create infobase (code: $($proc.ExitCode))"
exit 1
}
@@ -1318,10 +1525,11 @@ if ($hasRefTypes) {
# LoadConfigFromFiles
Write-Host "Loading configuration from files..."
$loadLog = Join-Path $env:TEMP "stub_load_log.txt"
$loadArgs = "DESIGNER /F`"$TempBasePath`" /LoadConfigFromFiles `"$cfgDir`" /Out `"$loadLog`" /DisableStartupDialogs"
$proc = Start-Process -FilePath $V8Path -ArgumentList $loadArgs -NoNewWindow -Wait -PassThru
$loadArgs = "DESIGNER /F`"$TempBasePath`" /LoadConfigFromFiles `"$cfgDir`" /Out `"$loadLog`" /DisableStartupDialogs" + $extraArgString
$proc = Invoke-PlatformProcess $V8Path @($loadArgs) -PreQuoted
if ($proc.ExitCode -ne 0) {
if (Test-Path $loadLog) { Get-Content $loadLog -Raw -ErrorAction SilentlyContinue | Write-Host }
Write-PlatformOutput $proc.Output
Write-Error "Failed to load config (code: $($proc.ExitCode))"
exit 1
}
@@ -1329,10 +1537,11 @@ if ($hasRefTypes) {
# UpdateDBCfg
Write-Host "Updating database configuration..."
$updateLog = Join-Path $env:TEMP "stub_update_log.txt"
$updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs"
$proc = Start-Process -FilePath $V8Path -ArgumentList $updateArgs -NoNewWindow -Wait -PassThru
$updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs" + $extraArgString
$proc = Invoke-PlatformProcess $V8Path @($updateArgs) -PreQuoted
if ($proc.ExitCode -ne 0) {
if (Test-Path $updateLog) { Get-Content $updateLog -Raw -ErrorAction SilentlyContinue | Write-Host }
Write-PlatformOutput $proc.Output
Write-Error "Failed to update DB config (code: $($proc.ExitCode))"
exit 1
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -20,6 +20,75 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -30,7 +99,167 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def new_uuid():
@@ -802,7 +1031,17 @@ def main():
parser.add_argument('-SourceDir', required=True)
parser.add_argument('-V8Path', required=True)
parser.add_argument('-TempBasePath', default='')
args = parser.parse_args()
parser.add_argument('-AdditionalV8Arguments', nargs='*', default=[],
help='Extra 1cv8 arguments, e.g. /UseHwLicenses+')
parser.add_argument('-AdditionalIbcmdArguments', nargs='*', default=[],
help='Extra ibcmd arguments in --key=value form')
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.SourceDir = clean_path(args.SourceDir, "-SourceDir")
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.TempBasePath = clean_path(args.TempBasePath, "-TempBasePath")
type_map = scan_ref_types(args.SourceDir)
register_columns = scan_register_columns(args.SourceDir)
@@ -1057,6 +1296,10 @@ def main():
# Stub via ibcmd (one call: create [--import --apply])
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {"/F": "-TempBasePath", "--db-path": "-TempBasePath"}
extra_args = resolve_extra_args(stub_engine, v8_extra, ibcmd_extra, arg_hints)
if stub_engine == "ibcmd":
import shutil
print(f'Creating infobase (ibcmd): {temp_base}')
@@ -1065,6 +1308,7 @@ def main():
if has_ref_types:
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
ib_args.append(f'--data={ib_data}')
ib_args.extend(extra_args)
result = run_ibcmd(ib_args, warn_no_user=False)
shutil.rmtree(ib_data, ignore_errors=True)
if result.returncode != 0:
@@ -1083,11 +1327,10 @@ def main():
# Create infobase
print(f'Creating infobase: {temp_base}')
result = subprocess.run(
[args.V8Path, 'CREATEINFOBASE', f'File={temp_base}', '/DisableStartupDialogs'],
capture_output=True, text=True,
)
result = run_v8(args.V8Path, ['CREATEINFOBASE', f'File="{temp_base}"', '/DisableStartupDialogs']
+ [quote_if_needed(a) for a in extra_args])
if result.returncode != 0:
print_platform_output(result)
print(f'Failed to create infobase (code: {result.returncode})', file=sys.stderr)
sys.exit(1)
@@ -1095,21 +1338,18 @@ def main():
cfg_dir = os.path.join(temp_base, 'cfg')
# LoadConfigFromFiles
print('Loading configuration from files...')
result = subprocess.run(
[args.V8Path, 'DESIGNER', f'/F{temp_base}', '/LoadConfigFromFiles', cfg_dir, '/DisableStartupDialogs'],
capture_output=True, text=True,
)
result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/LoadConfigFromFiles', f'"{cfg_dir}"',
'/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args])
if result.returncode != 0:
print_platform_output(result)
print(f'Failed to load config (code: {result.returncode})', file=sys.stderr)
sys.exit(1)
# UpdateDBCfg
print('Updating database configuration...')
update_log = os.path.join(tempfile.gettempdir(), 'stub_update_log.txt')
result = subprocess.run(
[args.V8Path, 'DESIGNER', f'/F{temp_base}', '/UpdateDBCfg', '/Out', update_log, '/DisableStartupDialogs'],
capture_output=True, text=True,
)
result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/UpdateDBCfg', '/Out', f'"{update_log}"',
'/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args])
if result.returncode != 0:
if os.path.isfile(update_log):
try:
@@ -1117,6 +1357,7 @@ def main():
print(f.read())
except Exception:
pass
print_platform_output(result)
print(f'Failed to update DB config (code: {result.returncode})', file=sys.stderr)
sys.exit(1)
+2
View File
@@ -55,6 +55,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <па
| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу |
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
+264 -20
View File
@@ -1,4 +1,4 @@
# epf-dump v1.6 — Dump external data processor or report (EPF/ERF) to XML sources
# epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -36,6 +36,12 @@
.PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
@@ -71,12 +77,177 @@ param(
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical"
[string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$InputFile = ConvertTo-CleanPath $InputFile '-InputFile'
$OutputDir = ConvertTo-CleanPath $OutputDir '-OutputDir'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -128,34 +299,95 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
function Test-DirNonEmpty {
# Postcondition: the platform must have written files into the output directory.
# Exit code 0 with an empty dir (broken/headless env) is a false success — reject it.
param([string]$Path)
return (Test-Path $Path -PathType Container) -and ([bool](Get-ChildItem -LiteralPath $Path -Force -ErrorAction SilentlyContinue | Select-Object -First 1))
}
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
@@ -189,16 +421,21 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $OutputDir)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "External data processor/report dumped successfully to: $OutputDir" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no files under $OutputDir — dump produced no output" -ForegroundColor Red
} else {
Write-Host "Error dumping external data processor/report (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -222,15 +459,21 @@ try {
$outFile = Join-Path $tempDir "dump_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
# Postcondition: exit 0 with an empty output directory is a false success.
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $OutputDir)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no files under $OutputDir — dump produced no output" -ForegroundColor Red
} else {
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red
}
@@ -243,6 +486,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+330 -23
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# epf-dump v1.6 — Dump external data processor or report (EPF/ERF) to XML sources
# epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys
import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -36,6 +58,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +354,25 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def dir_nonempty(path):
"""Postcondition: the platform must have written files into the output directory.
Exit code 0 with an empty dir (broken/headless env) is a false success reject it."""
return os.path.isdir(path) and any(os.scandir(path))
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -120,12 +396,36 @@ def main():
choices=["Hierarchical", "Plain"],
help="Dump format (default: Hierarchical)",
)
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.InputFile = clean_path(args.InputFile, "-InputFile")
args.OutputDir = clean_path(args.OutputDir, "-OutputDir")
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate database connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
@@ -163,51 +463,57 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
if result.returncode == 0:
exit_code = result.returncode
out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"External data processor/report dumped successfully to: {args.OutputDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
else:
print(f"Error dumping external data processor/report (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
print(f"Error dumping external data processor/report (code: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", args.InfoBasePath]
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments += ["/DumpExternalDataProcessorOrReportToFiles", args.OutputDir, args.InputFile]
arguments += ["/DumpExternalDataProcessorOrReportToFiles", f'"{args.OutputDir}"', f'"{args.InputFile}"']
arguments += ["-Format", args.Format]
# --- Output ---
out_file = os.path.join(temp_dir, "dump_log.txt")
arguments += ["/Out", out_file]
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
# Postcondition: exit 0 with an empty output directory is a false success.
out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Dump completed successfully to: {args.OutputDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
else:
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
@@ -222,6 +528,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+8 -7
View File
@@ -18,19 +18,20 @@ allowed-tools:
## Usage
```
/epf-init <Name> [Synonym] [SrcDir]
/epf-init <Name> [Synonym] [SrcDir] [FormatVersion]
```
| Параметр | Обязательный | По умолчанию | Описание |
|-----------|:------------:|--------------|-------------------------------------|
| Name | да | — | Имя обработки (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
| Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|------------------------------------------------|
| Name | да | — | Имя обработки (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
| FormatVersion | нет | `2.17` | Версия формата: 2.20 — платформа 8.3.27, 2.21 — 8.5. Дефолт открывается любой платформой |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<2.17|2.18|2.19|2.20|2.21>"]
```
## Дальнейшие шаги
+40 -6
View File
@@ -1,4 +1,4 @@
# epf-init v1.1 — Init 1C external data processor scaffold
# epf-init v1.7 — Init 1C external data processor scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -6,10 +6,22 @@ param(
[string]$Synonym = $Name,
[string]$SrcDir = "src"
[string]$SrcDir = "src",
# Версия формата выгрузки. Своей конфигурации у автономной обработки нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри обработки
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
[string]$FormatVersion = "2.17"
)
$ErrorActionPreference = "Stop"
function Esc-XmlText {
param([string]$s)
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
@@ -18,9 +30,17 @@ $uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString()
$uuid4 = [guid]::NewGuid().ToString()
# Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style):
# платформа держит объявления по алфавиту, дописать в конец нельзя.
if (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221) {
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
$xml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<MetaDataObject $xmlnsDecl version="$FormatVersion">
<ExternalDataProcessor uuid="$uuid1">
<InternalInfo>
<xr:ContainedObject>
@@ -33,11 +53,11 @@ $xml = @"
</xr:GeneratedType>
</InternalInfo>
<Properties>
<Name>$Name</Name>
<Name>$(Esc-XmlText $Name)</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content>
<v8:content>$(Esc-XmlText $Synonym)</v8:content>
</v8:item>
</Synonym>
<Comment/>
@@ -64,7 +84,16 @@ $extDir = Join-Path $processorDir "Ext"
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile (Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml") $xml $enc
# --- Модуль объекта ---
@@ -83,6 +112,11 @@ $moduleBsl = @"
"@
$modulePath = Join-Path $extDir "ObjectModule.bsl"
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
# самого скрипта, а он в репозитории хранится с LF.
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
Write-Host "[OK] Создана обработка: $rootFile"
+88 -10
View File
@@ -1,19 +1,61 @@
#!/usr/bin/env python3
# epf-init v1.1 — Init 1C external data processor scaffold
# epf-init v1.7 — Init 1C external data processor scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external data processor."""
import sys, os, argparse, uuid
import sys, os, re, argparse, uuid
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def esc_xml_text(s):
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми сознательно: разошедшиеся копии сводят на нет весь смысл.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -21,7 +63,12 @@ def main():
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
args = parser.parse_args()
# Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
args = ci_parse_args(parser)
name = args.Name
synonym = args.Synonym if args.Synonym else name
@@ -32,8 +79,36 @@ def main():
uuid3 = new_uuid()
uuid4 = new_uuid()
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
xmlns_decl = (
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
format_version = args.FormatVersion
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед
# style): платформа держит объявления по алфавиту, дописать в конец нельзя.
if format_rank(format_version) >= 221:
xmlns_decl = xmlns_decl.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<MetaDataObject {xmlns_decl} version="{format_version}">
\t<ExternalDataProcessor uuid="{uuid1}">
\t\t<InternalInfo>
\t\t\t<xr:ContainedObject>
@@ -46,11 +121,11 @@ def main():
\t\t\t</xr:GeneratedType>
\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Name>{esc_xml_text(name)}</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
@@ -72,7 +147,7 @@ def main():
ext_dir = os.path.join(processor_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
write_xml_file(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
# --- Модуль объекта ---
module_bsl = """\
@@ -89,7 +164,10 @@ def main():
#КонецОбласти"""
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
write_utf8_bom(module_path, module_bsl)
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без).
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
print(f"[OK] Создана обработка: {root_file}")
print(f" Каталог: {processor_dir}")
@@ -1,4 +1,4 @@
# epf-validate v1.2 — Validate 1C external data processor / report structure
# epf-validate v1.4 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
param(
@@ -185,8 +185,9 @@ if ($root.NamespaceURI -ne $expectedNs) {
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
}
# Detect type: ExternalDataProcessor or ExternalReport
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# epf-validate v1.2 — Validate 1C external data processor / report structure
# epf-validate v1.4 — 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
@@ -10,6 +10,28 @@ import sys
from io import StringIO
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
V8_NS = "http://v8.1c.ru/8.1/data/core"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
@@ -50,7 +72,7 @@ def main():
parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30)
parser.add_argument("-OutFile", default=None)
args = parser.parse_args()
args = ci_parse_args(parser)
max_errors = args.MaxErrors
@@ -165,8 +187,9 @@ def main():
version = root.get("version", "")
if not version:
report_warn("1. Missing version attribute on MetaDataObject")
elif version not in ("2.17", "2.20", "2.21"):
report_warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
elif version not in ("2.17", "2.18", "2.19", "2.20", "2.21"):
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
report_warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
# Detect type
child_elements = []
+2
View File
@@ -57,6 +57,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu
| `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
+2
View File
@@ -57,6 +57,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dum
| `-InputFile <путь>` | да | Путь к ERF-файлу |
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
+9 -8
View File
@@ -18,20 +18,21 @@ allowed-tools:
## Usage
```
/erf-init <Name> [Synonym] [SrcDir] [--with-skd]
/erf-init <Name> [Synonym] [SrcDir] [FormatVersion] [--with-skd]
```
| Параметр | Обязательный | По умолчанию | Описание |
|-----------|:------------:|--------------|---------------------------------------|
| Name | да | — | Имя отчёта (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
| Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|---------------------------------------|
| Name | да | — | Имя отчёта (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
| FormatVersion | нет | `2.17` | Версия формата: 2.20 — платформа 8.3.27, 2.21 — 8.5. Дефолт открывается любой платформой |
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<2.17|2.18|2.19|2.20|2.21>"] [-WithSKD]
```
## Дальнейшие шаги
+43 -9
View File
@@ -1,4 +1,4 @@
# erf-init v1.1 — Init 1C external report scaffold
# erf-init v1.7 — Init 1C external report scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -8,10 +8,22 @@ param(
[string]$SrcDir = "src",
[switch]$WithSKD
[switch]$WithSKD,
# Версия формата выгрузки. Своей конфигурации у автономного отчёта нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри отчёта
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
[string]$FormatVersion = "2.17"
)
$ErrorActionPreference = "Stop"
function Esc-XmlText {
param([string]$s)
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
@@ -20,6 +32,14 @@ $uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString()
$uuid4 = [guid]::NewGuid().ToString()
# Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style):
# платформа держит объявления по алфавиту, дописать в конец нельзя.
if (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221) {
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
# --- Формируем Properties ---
$mainDCSValue = ""
@@ -48,7 +68,7 @@ $childObjectsXml = if ($childObjectsContent) {
$xml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<MetaDataObject $xmlnsDecl version="$FormatVersion">
<ExternalReport uuid="$uuid1">
<InternalInfo>
<xr:ContainedObject>
@@ -61,11 +81,11 @@ $xml = @"
</xr:GeneratedType>
</InternalInfo>
<Properties>
<Name>$Name</Name>
<Name>$(Esc-XmlText $Name)</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content>
<v8:content>$(Esc-XmlText $Synonym)</v8:content>
</v8:item>
</Synonym>
<Comment/>
@@ -98,7 +118,16 @@ $extDir = Join-Path $reportDir "Ext"
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile (Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml") $xml $enc
# --- Модуль объекта ---
@@ -117,6 +146,11 @@ $moduleBsl = @"
"@
$modulePath = Join-Path $extDir "ObjectModule.bsl"
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
# самого скрипта, а он в репозитории хранится с LF.
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
Write-Host "[OK] Создан отчёт: $rootFile"
@@ -136,7 +170,7 @@ if ($WithSKD) {
$skdMetaXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<MetaDataObject $xmlnsDecl version="$FormatVersion">
<Template uuid="$skdUuid">
<Properties>
<Name>$skdName</Name>
@@ -153,7 +187,7 @@ if ($WithSKD) {
</MetaDataObject>
"@
[System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc)
Write-XmlFile $skdMetaPath $skdMetaXml $enc
$skdContent = @"
<?xml version="1.0" encoding="UTF-8"?>
@@ -173,7 +207,7 @@ if ($WithSKD) {
"@
$skdFilePath = Join-Path $skdExtDir "Template.xml"
[System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc)
Write-XmlFile $skdFilePath $skdContent $enc
Write-Host " СКД: $skdMetaPath"
Write-Host " Тело: $skdFilePath"
+91 -13
View File
@@ -1,19 +1,61 @@
#!/usr/bin/env python3
# erf-init v1.1 — Init 1C external report scaffold
# erf-init v1.7 — Init 1C external report scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external report."""
import sys, os, argparse, uuid
import sys, os, re, argparse, uuid
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def esc_xml_text(s):
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми сознательно: разошедшиеся копии сводят на нет весь смысл.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -21,8 +63,13 @@ def main():
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
# Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
parser.add_argument('-WithSKD', dest='WithSKD', action='store_true')
args = parser.parse_args()
args = ci_parse_args(parser)
name = args.Name
synonym = args.Synonym if args.Synonym else name
@@ -33,6 +80,34 @@ def main():
uuid3 = new_uuid()
uuid4 = new_uuid()
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
xmlns_decl = (
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
format_version = args.FormatVersion
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед
# style): платформа держит объявления по алфавиту, дописать в конец нельзя.
if format_rank(format_version) >= 221:
xmlns_decl = xmlns_decl.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
# --- Properties ---
main_dcs_value = ""
child_objects_content = ""
@@ -45,7 +120,7 @@ def main():
child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>"
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<MetaDataObject {xmlns_decl} version="{format_version}">
\t<ExternalReport uuid="{uuid1}">
\t\t<InternalInfo>
\t\t\t<xr:ContainedObject>
@@ -58,11 +133,11 @@ def main():
\t\t\t</xr:GeneratedType>
\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Name>{esc_xml_text(name)}</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
@@ -90,7 +165,7 @@ def main():
ext_dir = os.path.join(report_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
write_xml_file(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
# --- Модуль объекта ---
module_bsl = """\
@@ -107,7 +182,10 @@ def main():
#КонецОбласти"""
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
write_utf8_bom(module_path, module_bsl)
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без).
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
print(f"[OK] Создан отчёт: {root_file}")
print(f" Каталог: {report_dir}")
@@ -124,7 +202,7 @@ def main():
skd_uuid = new_uuid()
skd_meta_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<MetaDataObject {xmlns_decl} version="{format_version}">
\t<Template uuid="{skd_uuid}">
\t\t<Properties>
\t\t\t<Name>{skd_name}</Name>
@@ -140,7 +218,7 @@ def main():
\t</Template>
</MetaDataObject>'''
write_utf8_bom(skd_meta_path, skd_meta_xml)
write_xml_file(skd_meta_path, skd_meta_xml)
skd_content = '''<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
@@ -158,7 +236,7 @@ def main():
</DataCompositionSchema>'''
skd_file_path = os.path.join(skd_ext_dir, "Template.xml")
write_utf8_bom(skd_file_path, skd_content)
write_xml_file(skd_file_path, skd_content)
print(f" СКД: {skd_meta_path}")
print(f" Тело: {skd_file_path}")
+113 -19
View File
@@ -1,4 +1,4 @@
# form-add v1.7 — Add managed form to 1C config object
# form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -33,6 +33,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -69,10 +79,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -141,9 +154,20 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
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) {
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
@@ -153,6 +177,13 @@ function Detect-FormatVersion([string]$dir) {
return "2.17"
}
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# --- Фаза 1: Определение типа объекта ---
# Resolve ObjectPath (directory → .xml)
@@ -174,7 +205,26 @@ if (-not (Test-Path $ObjectPath)) {
$objectXmlFull = Resolve-Path $ObjectPath
Assert-EditAllowed $objectXmlFull.Path 'editable'
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent)
# Версию берём прежде всего из корня самого объекта — он её несёт всегда, а у автономной
# внешней обработки/отчёта подниматься к Configuration.xml просто некуда.
$script:formatVersion = $null
$objHead = [System.IO.File]::ReadAllText($objectXmlFull.Path, [System.Text.Encoding]::UTF8)
$objHead = $objHead.Substring(0, [Math]::Min(2000, $objHead.Length))
if ($objHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { $script:formatVersion = $Matches[1] }
if (-not $script:formatVersion) { $script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent) }
# Объявления пространств имён — одной переменной на корень: места эмиссии их только
# интерполируют. Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
$script:formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
if ((Get-FormatRank $script:formatVersion) -ge 221) {
$script:xmlnsDecl = $script:xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
$script:formNsDecl = $script:formNsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
@@ -195,7 +245,7 @@ $supportedTypes = @(
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task"
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal"
)
$objectType = $null
@@ -297,9 +347,16 @@ if ($objectType -in $processorLikeTypes) {
$extPresentationLine = "`n`t`t`t<ExtendedPresentation/>"
}
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
# сразу после UsePurposes (проверено по выгрузке 8.5, до ExtendedPresentation).
$useInIfcLine = ""
if ((Get-FormatRank $script:formatVersion) -ge 221) {
$useInIfcLine = "`n`t`t`t<UseInInterfaceCompatibilityMode>Any</UseInInterfaceCompatibilityMode>"
}
$formMetaXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$($script:formatVersion)">
<MetaDataObject $($script:xmlnsDecl) version="$($script:formatVersion)">
<Form uuid="$formUuid">
<Properties>
<Name>$FormName</Name>
@@ -315,20 +372,29 @@ $formMetaXml = @"
<UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
</UsePurposes>$extPresentationLine
</UsePurposes>$useInIfcLine$extPresentationLine
</Properties>
</Form>
</MetaDataObject>
"@
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
#
# Модуль .bsl сюда НЕ идёт — он пишется отдельно.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile $formMetaPath $formMetaXml $encBom
# --- 3b. Form.xml ---
$formXmlPath = Join-Path $formExtDir "Form.xml"
$formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
# Динамический список
# MainTable: тип.имя
@@ -336,7 +402,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)">
<Form $($script:formNsDecl) version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
@@ -361,7 +427,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)">
<Form $($script:formNsDecl) version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
@@ -408,7 +474,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)">
<Form $($script:formNsDecl) version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
@@ -428,7 +494,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
if (Test-Path $formXmlPath) {
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
} else {
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
Write-XmlFile $formXmlPath $formXml $encBom
}
# --- 3c. Module.bsl ---
@@ -460,6 +526,11 @@ $moduleBsl = @"
if (Test-Path $modulePath) {
Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting"
} else {
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
# самого скрипта, а он в репозитории хранится с LF.
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
}
@@ -471,7 +542,10 @@ if (-not $childObjects) {
exit 1
}
# Добавить <Form>$FormName</Form>
# Добавить <Form>$FormName</Form> — идемпотентно (не дублировать уже зарегистрированную)
$alreadyRegistered = [bool]$childObjects.SelectSingleNode("md:Form[text()='$FormName']", $nsMgr)
if (-not $alreadyRegistered) {
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formElem.InnerText = $FormName
@@ -525,6 +599,7 @@ if ($insertBefore) {
}
}
}
}
# --- SetDefault ---
@@ -565,12 +640,27 @@ if ($SetDefault -or $isFirstFormForPurpose) {
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom
$settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
$stream = New-Object System.IO.FileStream($objectXmlFull.Path, [System.IO.FileMode]::Create)
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$xmlDoc.Save($writer)
$writer.Close()
$stream.Close()
$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 $objectXmlFull.Path) -and ([System.IO.File]::ReadAllText($objectXmlFull.Path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($objectXmlFull.Path, $xmlText, $encBom)
# --- Фаза 5: Вывод ---
@@ -590,7 +680,11 @@ Write-Host " Metadata: $objDirName\$objBaseName\Forms\$FormName.xml"
Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml"
Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
Write-Host ""
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
if ($alreadyRegistered) {
Write-Host "Already registered: <Form>$FormName</Form> in ChildObjects (skipped duplicate)"
} else {
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
}
if ($defaultUpdated) {
Write-Host "${defaultPropName}: $defaultValue"
}
+230 -90
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-add v1.7 — Add managed form to 1C config object
# form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -11,6 +11,28 @@ import uuid
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
@@ -32,6 +54,18 @@ def _sg_root_uuid(xml_path):
return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
@@ -71,6 +105,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
@@ -78,6 +115,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12):
if not d:
break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
@@ -179,6 +218,16 @@ NSMAP = {
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:
@@ -193,21 +242,78 @@ def detect_format_version(d):
return "2.17"
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
def 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 _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None файл новый (сохранить текущее поведение)."""
try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → CRLF, канон #57)
if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_with_bom(tree, path):
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми сознательно: разошедшиеся копии сводят на нет весь смысл.
Модуль .bsl сюда НЕ идёт он пишется отдельно.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def main():
@@ -219,7 +325,7 @@ def main():
parser.add_argument("-Synonym", default=None)
parser.add_argument("-Purpose", default="Object")
parser.add_argument("-SetDefault", action="store_true")
args = parser.parse_args()
args = ci_parse_args(parser)
object_path = args.ObjectPath
form_name = args.FormName
@@ -246,7 +352,64 @@ def main():
object_xml_full = os.path.abspath(object_path)
assert_edit_allowed(object_xml_full, "editable")
format_version = detect_format_version(os.path.dirname(object_xml_full))
# Версию берём прежде всего из корня самого объекта — он её несёт всегда, а у автономной
# внешней обработки/отчёта подниматься к Configuration.xml просто некуда.
format_version = None
with open(object_xml_full, "r", encoding="utf-8-sig") as f:
obj_head = f.read(2000)
m_ver = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', obj_head)
if m_ver:
format_version = m_ver.group(1)
if not format_version:
format_version = detect_format_version(os.path.dirname(object_xml_full))
# Объявления пространств имён — одной переменной на корень: места эмиссии их только
# подставляют. Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
xmlns_decl = (
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
form_ns_decl = (
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
if format_rank(format_version) >= 221:
pal = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
xmlns_decl = xmlns_decl.replace(' xmlns:style=', pal)
form_ns_decl = form_ns_decl.replace(' xmlns:style=', pal)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(object_xml_full, parser_xml)
@@ -256,7 +419,7 @@ def main():
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task",
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal",
]
object_type = None
@@ -335,24 +498,7 @@ def main():
form_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
f' version="{format_version}">\n'
f'<MetaDataObject {xmlns_decl} version="{format_version}">\n'
f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{form_name}</Name>\n'
@@ -369,37 +515,22 @@ def main():
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n'
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
# сразу после UsePurposes (проверено по выгрузке 8.5, до ExtendedPresentation).
+ ('\t\t\t<UseInInterfaceCompatibilityMode>Any</UseInInterfaceCompatibilityMode>\n'
if format_rank(format_version) >= 221 else '')
+ ('\t\t\t<ExtendedPresentation/>\n' if object_type in processor_like_types else '')
+ '\t\t</Properties>\n'
'\t</Form>\n'
'</MetaDataObject>'
)
write_text_with_bom(form_meta_path, form_meta_xml)
write_xml_file(form_meta_path, form_meta_xml)
# --- 3b. Form.xml ---
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
form_ns_decl = (
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
if purpose in ("List", "Choice"):
# Dynamic list
main_table = f"{object_type}.{object_name}"
@@ -498,7 +629,7 @@ def main():
if os.path.exists(form_xml_path):
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
else:
write_text_with_bom(form_xml_path, form_xml)
write_xml_file(form_xml_path, form_xml)
# --- 3c. Module.bsl ---
@@ -529,7 +660,10 @@ def main():
if os.path.exists(module_path):
print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting")
else:
write_text_with_bom(module_path, module_bsl)
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без).
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
# --- Phase 4: Register in parent object ---
@@ -539,47 +673,50 @@ def main():
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
sys.exit(1)
# Add <Form>$FormName</Form>
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
# Add <Form>$FormName</Form> — idempotent (do not duplicate already-registered form)
already_registered = child_objects.find(f"md:Form[.='{form_name}']", NSMAP) is not None
# Find first <Template> to insert before it
first_template = child_objects.find("md:Template", NSMAP)
# Find first <TabularSection> to insert before it (if no Template)
first_tabular = child_objects.find("md:TabularSection", NSMAP)
if not already_registered:
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
# Determine insertion point: before Template, before TabularSection, or at end
insert_before = None
if first_template is not None:
insert_before = first_template
elif first_tabular is not None:
insert_before = first_tabular
# Find first <Template> to insert before it
first_template = child_objects.find("md:Template", NSMAP)
# Find first <TabularSection> to insert before it (if no Template)
first_tabular = child_objects.find("md:TabularSection", NSMAP)
if insert_before is not None:
# Insert before the found element
idx = list(child_objects).index(insert_before)
child_objects.insert(idx, form_elem)
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
form_elem.tail = "\n\t\t\t"
else:
# Add to end of ChildObjects
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# Determine insertion point: before Template, before TabularSection, or at end
insert_before = None
if first_template is not None:
insert_before = first_template
elif first_tabular is not None:
insert_before = first_tabular
if insert_before is not None:
# Insert before the found element
idx = list(child_objects).index(insert_before)
child_objects.insert(idx, form_elem)
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
form_elem.tail = "\n\t\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
# Add to end of ChildObjects
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# --- SetDefault ---
@@ -624,7 +761,10 @@ def main():
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
print()
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
if already_registered:
print(f"Already registered: <Form>{form_name}</Form> in ChildObjects (skipped duplicate)")
else:
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
if default_updated:
print(f"{default_prop_name}: {default_value}")
print()
+3 -2
View File
@@ -187,6 +187,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
| `showTitle: true` | Показывать заголовок группы |
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
| `controlRepresentation` | Отображение управления свёрткой (`behavior: "collapsible"`): `"TitleHyperlink"` (гиперссылка заголовка, по умолчанию) / `"Picture"` (картинка) |
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
| `children: [...]` | Вложенные элементы |
@@ -549,8 +550,8 @@ PictureField, привязанный к булеву/числу, рисует и
## Workflow
1. **Компиляция**: `/form-compile` генерирует `Form.xml` и автоматически регистрирует `<Form>` в `ChildObjects` родительского объекта (если OutputPath следует конвенции `.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml`).
2. **Метаданные формы** (`ФормаСписка.xml`) и `Module.bsl` создаёт `/form-add`. Если `/form-add` ещё не вызывался — вызови после `/form-compile`. Он не перезаписывает существующий Form.xml.
1. **Каркас**: `/form-add` создаёт метаданные формы (`ФормаСписка.xml`), `Module.bsl` и регистрирует форму у объекта.
2. **Компиляция**: `/form-compile` наполняет `Form.xml` элементами.
3. **Проверка**: `/form-validate`, `/form-info`.
## Верификация
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
# form-decompile v0.147 — Decompile 1C managed Form.xml to JSON DSL (draft)
# form-decompile v0.148 — Decompile 1C managed Form.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
param(
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-decompile v0.147 — Decompile 1C managed Form.xml to JSON DSL (draft)
# form-decompile v0.148 — Decompile 1C managed Form.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
#
@@ -13,6 +13,28 @@ import xml.etree.ElementTree as ET
from collections import OrderedDict
from decimal import Decimal
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# --- 1. Namespaces ---
NS_LF = "http://v8.1c.ru/8.3/xcf/logform"
NS_V8 = "http://v8.1c.ru/8.1/data/core"
@@ -3115,7 +3137,7 @@ def main():
parser = argparse.ArgumentParser(description='Decompile 1C managed Form.xml to JSON DSL', allow_abbrev=False)
parser.add_argument('-FormPath', '-Path', dest='FormPath', type=str, required=True)
parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None)
args = parser.parse_args()
args = ci_parse_args(parser)
form_path = args.FormPath
output_path = args.OutputPath
+67 -12
View File
@@ -1,4 +1,4 @@
# form-edit v1.3 — Edit 1C managed form elements
# form-edit v1.14 — Edit 1C managed form elements (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -27,6 +27,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -63,10 +73,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -258,15 +271,24 @@ function X {
function Esc-Xml {
param([string]$s)
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
}
function Esc-XmlText {
# Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует —
# пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной &quot;). &quot; платформа
# принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе.
param([string]$s)
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
function Emit-MLText {
param([string]$tag, [string]$text, [string]$indent)
X "$indent<$tag>"
X "$indent`t<v8:item>"
X "$indent`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t<v8:content>$(Esc-Xml $text)</v8:content>"
X "$indent`t`t<v8:content>$(Esc-XmlText $text)</v8:content>"
X "$indent`t</v8:item>"
X "$indent</$tag>"
}
@@ -295,24 +317,49 @@ $script:formTypeSynonyms["бизнеспроцессссылка"] = "
$script:formTypeSynonyms["задачассылка"] = "TaskRef"
$script:formTypeSynonyms["определяемыйтип"] = "DefinedType"
# Алиас на локальный словарь: тело Resolve-TypeStr ниже — общая реализация,
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
$script:typeSynonyms = $script:formTypeSynonyms
function Resolve-TypeStr {
param([string]$typeStr)
if (-not $typeStr) { return $typeStr }
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
if ($typeStr.StartsWith('cfg:')) {
$typeStr = $typeStr.Substring(4)
} elseif ($typeStr.Contains('.') -and $typeStr -match '^d\d+p\d+:') {
$typeStr = $typeStr.Substring($typeStr.IndexOf(':') + 1)
}
# Параметризованные типы: Number(15,2), Строка(100)
if ($typeStr -match '^([^(]+)\((.+)\)$') {
$base = $Matches[1].Trim(); $params = $Matches[2]
$r = $script:formTypeSynonyms[$base.ToLower()]
if ($r) { return "$r($params)" }
$baseName = $Matches[1].Trim()
$params = $Matches[2]
$resolved = $script:typeSynonyms[$baseName.ToLower()]
if ($resolved) { return "$resolved($params)" }
return $typeStr
}
# Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации
if ($typeStr.Contains('.')) {
$i = $typeStr.IndexOf('.')
$prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i)
$r = $script:formTypeSynonyms[$prefix.ToLower()]
if ($r) { return "$r$suffix" }
$dotIdx = $typeStr.IndexOf('.')
$prefix = $typeStr.Substring(0, $dotIdx)
$suffix = $typeStr.Substring($dotIdx) # includes the dot
$resolved = $script:typeSynonyms[$prefix.ToLower()]
if ($resolved) { return "$resolved$suffix" }
return $typeStr
}
$r = $script:formTypeSynonyms[$typeStr.ToLower()]
if ($r) { return $r }
# Простое имя
$resolved = $script:typeSynonyms[$typeStr.ToLower()]
if ($resolved) { return $resolved }
return $typeStr
}
@@ -576,7 +623,7 @@ function Emit-Label {
X "$inner<Title formatted=`"$formatted`">"
X "$inner`t<v8:item>"
X "$inner`t`t<v8:lang>ru</v8:lang>"
X "$inner`t`t<v8:content>$(Esc-Xml "$($el.title)")</v8:content>"
X "$inner`t`t<v8:content>$(Esc-XmlText "$($el.title)")</v8:content>"
X "$inner`t</v8:item>"
X "$inner</Title>"
}
@@ -1371,8 +1418,16 @@ if ($def.elementEvents -and $def.elementEvents.Count -gt 0) {
$content = $xmlDoc.OuterXml
# Ensure encoding declaration is uppercase UTF-8
$content = $content -replace '^<\?xml version="1.0" encoding="utf-8"\?>', '<?xml version="1.0" encoding="UTF-8"?>'
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$content = [regex]::Replace($content, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$enc = New-Object System.Text.UTF8Encoding($true)
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $resolvedFormPath) -and ([System.IO.File]::ReadAllText($resolvedFormPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$content = ($content -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($resolvedFormPath, $content, $enc)
# === 14. Summary ===
+155 -21
View File
@@ -1,4 +1,4 @@
# form-edit v1.3 — Edit 1C managed form elements (Python port)
# form-edit v1.14 — 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
@@ -11,6 +11,65 @@ from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
# сохраняется — от него зависит порядок эмиссии.
def _actual(self, key):
if not isinstance(key, str) or dict.__contains__(self, key):
return key
ci = self.__dict__.get('_ci')
if ci is None or len(ci) != len(self):
ci = {k.lower(): k for k in self if isinstance(k, str)}
self.__dict__['_ci'] = ci
return ci.get(key.lower(), key)
def __getitem__(self, key):
return dict.__getitem__(self, self._actual(key))
def __contains__(self, key):
return dict.__contains__(self, self._actual(key))
def get(self, key, default=None):
return dict.get(self, self._actual(key), default)
def pop(self, key, *default):
return dict.pop(self, self._actual(key), *default)
def __setitem__(self, key, value):
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
dict.__setitem__(self, self._actual(key), value)
def ci_json(obj):
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
if isinstance(obj, dict):
return CIDict((k, ci_json(v)) for k, v in obj.items())
if isinstance(obj, list):
return [ci_json(v) for v in obj]
return obj
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
@@ -31,6 +90,18 @@ def _sg_root_uuid(xml_path):
return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
@@ -70,6 +141,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
@@ -77,6 +151,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12):
if not d:
break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
@@ -175,7 +251,7 @@ def assert_edit_allowed(target_path, require):
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-FormPath", "-Path", required=True)
parser.add_argument("-JsonPath", required=True)
args = parser.parse_args()
args = ci_parse_args(parser)
form_path = args.FormPath
json_path = args.JsonPath
@@ -209,9 +285,16 @@ def local_name(node):
# ── helpers ──────────────────────────────────────────────────
def esc_xml(s):
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def esc_xml_text(s):
"""Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует
(92142 сырых кавычки на корпус, ни одной &quot;); &quot; она принимает, но нормализует обратно."""
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
# ── 1. Load Form.xml ────────────────────────────────────────
if not os.path.exists(form_path):
@@ -235,7 +318,7 @@ root = tree.getroot()
# ── 2. Load JSON ────────────────────────────────────────────
with open(json_path, "r", encoding="utf-8-sig") as f:
defn = json.load(f)
defn = ci_json(json.load(f))
# ── 3. Form name + header ───────────────────────────────────
@@ -367,23 +450,48 @@ _FORM_TYPE_SYNONYMS = {
}
# Алиас на локальный словарь: тело resolve_type_str ниже — общая реализация,
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
TYPE_SYNONYMS = _FORM_TYPE_SYNONYMS
def resolve_type_str(type_str):
if not type_str:
return type_str
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
if type_str.startswith('cfg:'):
type_str = type_str[4:]
elif '.' in type_str and re.match(r'^d\d+p\d+:', type_str):
type_str = type_str[type_str.index(':') + 1:]
# Параметризованные типы: Number(15,2), Строка(100)
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
if m:
base, params = m.group(1).strip(), m.group(2)
r = _FORM_TYPE_SYNONYMS.get(base.lower())
return f"{r}({params})" if r else type_str
base_name = m.group(1).strip()
params = m.group(2)
resolved = TYPE_SYNONYMS.get(base_name.lower())
if resolved:
return f'{resolved}({params})'
return type_str
# Ссылочные типы: СправочникСсылка.Организации -> CatalogRef.Организации
if '.' in type_str:
i = type_str.index('.')
prefix, suffix = type_str[:i], type_str[i:]
r = _FORM_TYPE_SYNONYMS.get(prefix.lower())
return f"{r}{suffix}" if r else type_str
r = _FORM_TYPE_SYNONYMS.get(type_str.lower())
return r if r else type_str
dot_idx = type_str.index('.')
prefix = type_str[:dot_idx]
suffix = type_str[dot_idx:] # includes the dot
resolved = TYPE_SYNONYMS.get(prefix.lower())
if resolved:
return f'{resolved}{suffix}'
return type_str
# Простое имя
resolved = TYPE_SYNONYMS.get(type_str.lower())
if resolved:
return resolved
return type_str
def emit_type(type_str, indent):
if not type_str:
X(f"{indent}<Type/>")
@@ -477,7 +585,7 @@ def emit_mltext(tag, text, indent):
X(f"{indent}<{tag}>")
X(f"{indent}\t<v8:item>")
X(f"{indent}\t\t<v8:lang>ru</v8:lang>")
X(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>")
X(f"{indent}\t\t<v8:content>{esc_xml_text(text)}</v8:content>")
X(f"{indent}\t</v8:item>")
X(f"{indent}</{tag}>")
@@ -705,7 +813,7 @@ def emit_label(el, name, _id, indent):
X(f'{inner}<Title formatted="{formatted}">')
X(f"{inner}\t<v8:item>")
X(f"{inner}\t\t<v8:lang>ru</v8:lang>")
X(f"{inner}\t\t<v8:content>{esc_xml(str(el['title']))}</v8:content>")
X(f"{inner}\t\t<v8:content>{esc_xml_text(str(el['title']))}</v8:content>")
X(f"{inner}\t</v8:item>")
X(f"{inner}</Title>")
emit_common_flags(el, inner)
@@ -1458,14 +1566,40 @@ if elem_events_list:
# ── 13. Save ────────────────────────────────────────────────
# Round-trip: определить стиль исходного файла (на диске он ещё не перезаписан).
try:
_fe_raw = open(resolved_form_path, "rb").read()
except OSError:
_fe_raw = None
if _fe_raw is not None:
_fe_bom = _fe_raw.startswith(b"\xef\xbb\xbf")
_fe_body = _fe_raw[3:] if _fe_bom else _fe_raw
_fe_crlf = b"\r\n" in _fe_body
_fe_enc_m = re.search(rb'encoding="([^"]+)"', _fe_body[:200])
_fe_enc = _fe_enc_m.group(1).decode("ascii") if _fe_enc_m else "utf-8"
_fe_final_nl = _fe_body.endswith(b"\n")
else:
_fe_bom, _fe_crlf, _fe_enc, _fe_final_nl = True, False, "utf-8", True
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
# Fix XML declaration quotes
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
# Восстановить регистр encoding как в оригинале.
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + _fe_enc.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах).
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале.
xml_bytes = xml_bytes.rstrip(b"\n")
if _fe_final_nl:
xml_bytes += b"\n"
# Write with BOM
# EOL — как в оригинале.
if _fe_crlf:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
# Write preserving BOM as in original.
with open(resolved_form_path, "wb") as f:
f.write(b'\xef\xbb\xbf')
if _fe_bom:
f.write(b'\xef\xbb\xbf')
f.write(xml_bytes)
# ── 14. Summary ─────────────────────────────────────────────
+15 -2
View File
@@ -1,4 +1,4 @@
# form-info v1.4 — Analyze 1C managed form structure
# form-info v1.7 — Analyze 1C managed form structure (+единое имя хелпера состояния поддержки)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)]
@@ -372,6 +372,16 @@ if ($formsIdx -ge 0 -and ($formsIdx + 1) -lt $parts.Count) {
# See docs/1c-support-state-spec.md. Walks up from the target path, taking the
# uuid of the nearest element meta-xml (form/template/etc.) and the config root
# bin. Never throws — degrades to "не на поддержке".
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Get-SupportStatusForPath([string]$targetPath) {
try {
$rp = (Resolve-Path $targetPath).Path
@@ -390,8 +400,10 @@ function Get-SupportStatusForPath([string]$targetPath) {
}
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
$elemUuid = Get-RootUuid $rp
if (Test-ExternalObjectRoot $rp) { return $null }
$d = [System.IO.Path]::GetDirectoryName($rp)
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return $null }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $binPath) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -448,7 +460,8 @@ if ($formTitle) { $header += " — `"$formTitle`"" }
if ($objectContext) { $header += " ($objectContext)" }
$header += " ==="
$lines += $header
$lines += "Поддержка: $(Get-SupportStatusForPath $FormPath)"
$support = Get-SupportStatusForPath $FormPath
if ($null -ne $support) { $lines += "Поддержка: $support" }
# --- Form properties (Title excluded — shown in header) ---
+42 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-info v1.4 — Analyze 1C managed form structure
# form-info v1.7 — Analyze 1C managed form structure (+единое имя хелпера состояния поддержки)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -8,6 +8,28 @@ import re
import sys
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# --- Namespace map ---
NSMAP = {
@@ -353,14 +375,29 @@ def get_support_status_for_path(target_path):
except Exception:
pass
return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
rp = os.path.abspath(target_path)
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
elem_uuid = root_uuid(rp)
if _sg_is_external_root(rp):
return None
bin_path = None
d = os.path.dirname(rp)
for _ in range(12):
if not d:
break
if _sg_is_external_root(d + ".xml"):
return None
if not elem_uuid:
elem_uuid = root_uuid(d + ".xml")
if not bin_path:
@@ -418,7 +455,7 @@ def main():
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Line offset for pagination")
parser.add_argument("-Expand", default="", help="Expand collapsed section by name, or * for all")
args = parser.parse_args()
args = ci_parse_args(parser)
form_path = args.FormPath
limit = args.Limit
@@ -513,7 +550,9 @@ def main():
header += f" ({object_context})"
header += " ==="
lines.append(header)
lines.append(f"Поддержка: {get_support_status_for_path(form_path)}")
_support = get_support_status_for_path(form_path)
if _support is not None:
lines.append(f"Поддержка: {_support}")
# --- Form properties (Title excluded -- shown in header) ---
prop_names = [
@@ -1,4 +1,4 @@
# form-remove v1.3 — Remove form from 1C object
# form-remove v1.9 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -64,6 +64,10 @@ foreach ($node in $formNodes) {
$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 }
break
}
}
@@ -74,7 +78,9 @@ foreach ($node in $formNodes) {
$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) {
$node.InnerText = ""
# IsEmpty, а не InnerText="": пустая строка сериализуется парой <Tag></Tag>, а
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
$node.IsEmpty = $true
}
}
@@ -83,11 +89,26 @@ $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
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$xmlDoc.Save($writer)
$writer.Close()
$stream.Close()
$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)
Write-Host "[OK] Форма $FormName удалена из $rootXmlPath"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# remove-form v1.3 — Remove form from 1C object
# form-remove v1.9 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -10,17 +10,76 @@ import sys
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
def _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None файл новый (сохранить текущее поведение)."""
try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → CRLF, канон #57)
if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_with_bom(tree, path):
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
@@ -31,7 +90,7 @@ def main():
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
args = ci_parse_args(parser)
object_name = args.ObjectName
form_name = args.FormName
@@ -83,6 +142,10 @@ def main():
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
break
# Clear any Default*/Auxiliary* form slot that pointed to the removed form
@@ -93,7 +156,9 @@ def main():
if not isinstance(el.tag, str):
continue
if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text):
el.text = ""
# text=None, а не "": пустая строка сериализуется парой <Tag></Tag>, а
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
el.text = None
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
@@ -1,4 +1,4 @@
# form-validate v1.8 — Validate 1C managed form
# form-validate v1.10 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -127,10 +127,11 @@ if ($root.LocalName -ne "Form") {
Report-Error "Root element is '$($root.LocalName)', expected 'Form'"
} else {
$version = $root.GetAttribute("version")
if ($version -eq "2.17" -or $version -eq "2.20") {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
if ($version -in @("2.17", "2.18", "2.19", "2.20")) {
Report-OK "Root element: Form version=$version"
} elseif ($version) {
Report-Warn "Form version='$version' (expected 2.17 or 2.20)"
Report-Warn "Form version='$version' (expected 2.17-2.20)"
} else {
Report-Warn "Form version attribute missing"
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-validate v1.8 — Validate 1C managed form
# form-validate v1.10 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -8,6 +8,28 @@ import re
import sys
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
F_NS = "http://v8.1c.ru/8.3/xcf/logform"
V8_NS = "http://v8.1c.ru/8.1/data/core"
@@ -60,7 +82,7 @@ def main():
parser.add_argument("-FormPath", "-Path", required=True)
parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30)
args = parser.parse_args()
args = ci_parse_args(parser)
form_path = args.FormPath
detailed = args.Detailed
@@ -161,10 +183,11 @@ def main():
report_error(f"Root element is '{localname(root)}', expected 'Form'")
else:
version = root.get("version", "")
if version in ("2.17", "2.20"):
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
if version in ("2.17", "2.18", "2.19", "2.20"):
report_ok(f"Root element: Form version={version}")
elif version:
report_warn(f"Form version='{version}' (expected 2.17 or 2.20)")
report_warn(f"Form version='{version}' (expected 2.17-2.20)")
else:
report_warn("Form version attribute missing")
+57 -8
View File
@@ -1,4 +1,4 @@
# help-add v1.7 — Add built-in help to 1C object
# help-add v1.19 — Add built-in help to 1C object (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -28,6 +28,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -64,10 +74,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -136,10 +149,20 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
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) {
$content = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
$head = $content.Substring(0, [Math]::Min(2000, $content.Length))
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
@@ -182,7 +205,18 @@ $helpXml = @"
</Help>
"@
[System.IO.File]::WriteAllText($helpXmlPath, $helpXml, $encBom)
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
#
# HTML-страница сюда НЕ идёт — платформа хранит её с LF.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile $helpXmlPath $helpXml $encBom
# --- 2. Help/<lang>.html ---
@@ -242,11 +276,26 @@ if (Test-Path $formsDir) {
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom
$settings.Indent = $false
$stream = New-Object System.IO.FileStream($formMeta.FullName, [System.IO.FileMode]::Create)
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$xmlDoc.Save($writer)
$writer.Close()
$stream.Close()
$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 $formMeta.FullName) -and ([System.IO.File]::ReadAllText($formMeta.FullName) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($formMeta.FullName, $xmlText, $encBom)
Write-Host " IncludeHelpInContents добавлен: $($formMeta.Name)"
}
+114 -14
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# add-help v1.7 — Add built-in help to 1C object
# help-add v1.19 — Add built-in help to 1C object (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -10,6 +10,28 @@ import sys
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
@@ -33,6 +55,18 @@ def _sg_root_uuid(xml_path):
return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
@@ -72,6 +106,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
@@ -79,6 +116,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12):
if not d:
break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
@@ -174,6 +213,16 @@ def assert_edit_allowed(target_path, require):
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:
@@ -188,21 +237,72 @@ def detect_format_version(d):
return "2.17"
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
def _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None файл новый (сохранить текущее поведение)."""
try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → CRLF, канон #57)
if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_with_bom(tree, path):
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми сознательно: разошедшиеся копии сводят на нет весь смысл.
HTML-страница сюда НЕ идёт платформа хранит её с LF.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def main():
@@ -212,7 +312,7 @@ def main():
parser.add_argument("-ObjectName", required=True)
parser.add_argument("-Lang", default="ru")
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
args = ci_parse_args(parser)
object_name = args.ObjectName
lang = args.Lang
@@ -248,7 +348,7 @@ def main():
'</Help>'
)
write_text_with_bom(help_xml_path, help_xml)
write_xml_file(help_xml_path, help_xml)
# --- 2. Help/<lang>.html ---
@@ -271,7 +371,7 @@ def main():
'</html>'
)
write_text_with_bom(help_html_path, help_html)
write_utf8_bom(help_html_path, help_html)
# --- 3. Check IncludeHelpInContents in form metadata ---
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# img-grid v1.1 — Overlay numbered grid on image
# img-grid v1.2 — Overlay numbered grid on image
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Overlay a numbered grid on an image to help determine column/row proportions.
@@ -16,6 +16,28 @@ import os
import sys
from PIL import Image, ImageDraw, ImageFont
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
MARGIN_TOP = 20
MARGIN_LEFT = 24
@@ -30,7 +52,7 @@ def main():
parser.add_argument("-r", "--rows", type=int, default=0,
help="Number of horizontal divisions (0 = auto, match cell aspect ratio)")
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
args = parser.parse_args()
args = ci_parse_args(parser)
if args.cols <= 0:
parser.error("--cols must be greater than 0")
@@ -1,4 +1,4 @@
# interface-edit v1.6 — Edit 1C CommandInterface.xml
# interface-edit v1.18 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
@@ -39,6 +39,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -75,10 +85,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -149,9 +162,20 @@ Assert-EditAllowed $CIPath 'editable'
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) {
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
@@ -186,7 +210,12 @@ if (-not (Test-Path $CIPath)) {
</CommandInterface>
"@
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($CIPath, $emptyCI, $utf8Bom)
# Файл СОЗДАЁМ — пишем канон выгрузки: CRLF, без перевода строки в конце.
# (Правка существующего файла, наоборот, наследует его стиль — это делает
# основной путь сохранения ниже.) Нормализация нужна потому, что here-string
# берёт переводы строк из самого .ps1, а он в репозитории хранится с LF.
$emptyCI = ($emptyCI -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($CIPath, $emptyCI.TrimEnd("`r", "`n"), $utf8Bom)
Write-Host "[INFO] Created new CommandInterface.xml: $CIPath"
} else {
Write-Error "File not found: $CIPath (use -CreateIfMissing to create)"
@@ -367,6 +396,10 @@ $script:typeNormMap = @{
"ПланОбмена"="ExchangePlan"; "ЖурналДокументов"="DocumentJournal"
"ОбщийМодуль"="CommonModule"; "ОбщаяКоманда"="CommonCommand"
"ОбщаяФорма"="CommonForm"; "Подсистема"="Subsystem"
"РегистрРасчёта"="CalculationRegister"; "РегистрРасчета"="CalculationRegister"
"ПланВидовРасчёта"="ChartOfCalculationTypes"; "ПланВидовРасчета"="ChartOfCalculationTypes"
"Роль"="Role"; "ОбщийМакет"="CommonTemplate"; "ЭлементСтиля"="StyleItem"
"ОбщийРеквизит"="CommonAttribute"; "ГруппаКоманд"="CommandGroup"
# Russian plural
"Справочники"="Catalog"; "Документы"="Document"; "Перечисления"="Enum"
"Константы"="Constant"; "Отчёты"="Report"; "Отчеты"="Report"; "Обработки"="DataProcessor"
@@ -376,6 +409,10 @@ $script:typeNormMap = @{
"БизнесПроцессы"="BusinessProcess"; "Задачи"="Task"
"ПланыОбмена"="ExchangePlan"; "ЖурналыДокументов"="DocumentJournal"
"Подсистемы"="Subsystem"
"РегистрыРасчёта"="CalculationRegister"; "РегистрыРасчета"="CalculationRegister"
"ПланыВидовРасчёта"="ChartOfCalculationTypes"; "ПланыВидовРасчета"="ChartOfCalculationTypes"
"Роли"="Role"; "ОбщиеМакеты"="CommonTemplate"; "ЭлементыСтиля"="StyleItem"
"ОбщиеРеквизиты"="CommonAttribute"; "ГруппыКоманд"="CommandGroup"
}
function Normalize-CmdName([string]$name) {
@@ -658,8 +695,16 @@ $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 $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
Info "Saved: $resolvedPath"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# interface-edit v1.6 — Edit 1C CommandInterface.xml
# interface-edit v1.18 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -10,6 +10,65 @@ import subprocess
import sys
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
# сохраняется — от него зависит порядок эмиссии.
def _actual(self, key):
if not isinstance(key, str) or dict.__contains__(self, key):
return key
ci = self.__dict__.get('_ci')
if ci is None or len(ci) != len(self):
ci = {k.lower(): k for k in self if isinstance(k, str)}
self.__dict__['_ci'] = ci
return ci.get(key.lower(), key)
def __getitem__(self, key):
return dict.__getitem__(self, self._actual(key))
def __contains__(self, key):
return dict.__contains__(self, self._actual(key))
def get(self, key, default=None):
return dict.get(self, self._actual(key), default)
def pop(self, key, *default):
return dict.pop(self, self._actual(key), *default)
def __setitem__(self, key, value):
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
dict.__setitem__(self, self._actual(key), value)
def ci_json(obj):
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
if isinstance(obj, dict):
return CIDict((k, ci_json(v)) for k, v in obj.items())
if isinstance(obj, list):
return [ci_json(v) for v in obj]
return obj
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
@@ -31,6 +90,18 @@ def _sg_root_uuid(xml_path):
return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
@@ -70,6 +141,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
@@ -77,6 +151,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12):
if not d:
break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
@@ -172,6 +248,16 @@ def assert_edit_allowed(target_path, require):
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:
@@ -265,18 +351,55 @@ def import_ci_fragment(xml_string):
def parse_value_list(val):
val = val.strip()
if val.startswith("["):
arr = json.loads(val)
arr = ci_json(json.loads(val))
return [str(item) for item in arr]
return [val]
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
def _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None файл новый (сохранить текущее поведение)."""
try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → CRLF, канон #57)
if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_bom(tree, path):
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
@@ -304,6 +427,10 @@ TYPE_NORM_MAP = {
'ПланОбмена': 'ExchangePlan', 'ЖурналДокументов': 'DocumentJournal',
'ОбщийМодуль': 'CommonModule', 'ОбщаяКоманда': 'CommonCommand',
'ОбщаяФорма': 'CommonForm', 'Подсистема': 'Subsystem',
'РегистрРасчёта': 'CalculationRegister', 'РегистрРасчета': 'CalculationRegister',
'ПланВидовРасчёта': 'ChartOfCalculationTypes', 'ПланВидовРасчета': 'ChartOfCalculationTypes',
'Роль': 'Role', 'ОбщийМакет': 'CommonTemplate', 'ЭлементСтиля': 'StyleItem',
'ОбщийРеквизит': 'CommonAttribute', 'ГруппаКоманд': 'CommandGroup',
# Russian plural
'Справочники': 'Catalog', 'Документы': 'Document', 'Перечисления': 'Enum',
'Константы': 'Constant', 'Отчёты': 'Report', 'Отчеты': 'Report', 'Обработки': 'DataProcessor',
@@ -313,6 +440,10 @@ TYPE_NORM_MAP = {
'БизнесПроцессы': 'BusinessProcess', 'Задачи': 'Task',
'ПланыОбмена': 'ExchangePlan', 'ЖурналыДокументов': 'DocumentJournal',
'Подсистемы': 'Subsystem',
'РегистрыРасчёта': 'CalculationRegister', 'РегистрыРасчета': 'CalculationRegister',
'ПланыВидовРасчёта': 'ChartOfCalculationTypes', 'ПланыВидовРасчета': 'ChartOfCalculationTypes',
'Роли': 'Role', 'ОбщиеМакеты': 'CommonTemplate', 'ЭлементыСтиля': 'StyleItem',
'ОбщиеРеквизиты': 'CommonAttribute', 'ГруппыКоманд': 'CommandGroup',
}
@@ -348,7 +479,7 @@ def main():
parser.add_argument("-Value", default=None)
parser.add_argument("-CreateIfMissing", action="store_true")
parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args()
args = ci_parse_args(parser)
# --- Mode validation ---
if args.DefinitionFile and args.Operation:
@@ -385,7 +516,12 @@ def main():
f'\tversion="{format_version}">\n'
f'</CommandInterface>'
)
with open(ci_path, "w", encoding="utf-8-sig") as fh:
# Файл СОЗДАЁМ — пишем канон выгрузки: CRLF в разделителях. (Правка
# существующего файла, наоборот, наследует его стиль — это делает
# save_xml_bom через _detect_xml_style.) newline="" обязателен: без него
# текстовый режим дал бы CRLF на Windows и LF на macOS.
empty_ci = empty_ci.replace("\r\n", "\n").replace("\n", "\r\n").rstrip("\r\n")
with open(ci_path, "w", encoding="utf-8-sig", newline="") as fh:
fh.write(empty_ci)
print(f"[INFO] Created new CommandInterface.xml: {ci_path}")
else:
@@ -511,7 +647,7 @@ def main():
def do_place(json_val):
nonlocal add_count, modify_count
defn = json_val if isinstance(json_val, dict) else json.loads(json_val)
defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
cmd_name = normalize_cmd_name(str(defn["command"]))
group_name = str(defn["group"])
if not cmd_name or not group_name:
@@ -539,7 +675,7 @@ def main():
def do_order(json_val):
nonlocal add_count, remove_count
defn = json_val if isinstance(json_val, dict) else json.loads(json_val)
defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
group_name = str(defn["group"])
commands = [normalize_cmd_name(str(c)) for c in defn["commands"]]
if not group_name or not commands:
@@ -573,7 +709,7 @@ def main():
def do_subsystem_order(json_val):
nonlocal add_count, remove_count
parsed = json_val if isinstance(json_val, list) else json.loads(json_val)
parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
subsystems = [str(s) for s in parsed]
if not subsystems:
print("subsystem-order requires array of subsystem paths", file=sys.stderr)
@@ -598,7 +734,7 @@ def main():
def do_group_order(json_val):
nonlocal add_count, remove_count
parsed = json_val if isinstance(json_val, list) else json.loads(json_val)
parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
groups = [str(g) for g in parsed]
if not groups:
print("group-order requires array of group names", file=sys.stderr)
@@ -628,7 +764,7 @@ def main():
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 = json.loads(fh.read())
ops = ci_json(json.loads(fh.read()))
if isinstance(ops, list):
operations = ops
else:
@@ -638,19 +774,21 @@ def main():
for op in operations:
op_name = op.get("operation", args.Operation or "")
# PS сравнивает имя операции через switch, а он регистронезависим.
op_key = str(op_name).lower()
op_value = op.get("value", args.Value or "")
if op_name == "hide":
if op_key == "hide":
do_hide(parse_value_list(op_value))
elif op_name == "show":
elif op_key == "show":
do_show(parse_value_list(op_value))
elif op_name == "place":
elif op_key == "place":
do_place(op_value)
elif op_name == "order":
elif op_key == "order":
do_order(op_value)
elif op_name == "subsystem-order":
elif op_key == "subsystem-order":
do_subsystem_order(op_value)
elif op_name == "group-order":
elif op_key == "group-order":
do_group_order(op_value)
else:
print(f"Unknown operation: {op_name}", file=sys.stderr)
@@ -1,4 +1,4 @@
# interface-validate v1.1 — Validate 1C CommandInterface.xml structure
# interface-validate v1.3 — Validate 1C CommandInterface.xml structure (+Report-*: общий эталон вывода валидаторов)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
@@ -51,16 +51,21 @@ $script:output = New-Object System.Text.StringBuilder 8192
$script:allCommandNames = @()
function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null }
function Report-OK([string]$msg) {
function Report-OK {
param([string]$msg)
$script:okCount++
if ($Detailed) { Out-Line "[OK] $msg" }
}
function Report-Error([string]$msg) {
function Report-Error {
param([string]$msg)
$script:errors++
Out-Line "[ERROR] $msg"
if ($script:errors -ge $MaxErrors) { $script:stopped = $true }
if ($script:errors -ge $MaxErrors) {
$script:stopped = $true
}
}
function Report-Warn([string]$msg) {
function Report-Warn {
param([string]$msg)
$script:warnings++
Out-Line "[WARN] $msg"
}
@@ -1,10 +1,32 @@
#!/usr/bin/env python3
# interface-validate v1.1 — Validate 1C CommandInterface.xml structure
# interface-validate v1.3 — Validate 1C CommandInterface.xml structure (+Report-*: общий эталон вывода валидаторов)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates CommandInterface.xml sections, command references, order, duplicates."""
import sys, os, argparse, re
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
NS_CI = 'http://v8.1c.ru/8.3/xcf/extrnprops'
NS_XR = 'http://v8.1c.ru/8.3/xcf/readable'
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
@@ -83,7 +105,7 @@ def main():
parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args()
args = ci_parse_args(parser)
ci_path = args.CIPath
detailed = args.Detailed
+80 -61
View File
@@ -9,18 +9,19 @@ allowed-tools:
- Glob
---
# /meta-compile — генерация объектов метаданных из JSON DSL
# /meta-compile — генерация объектов метаданных из JSON
Принимает JSON-определение объекта метаданных → генерирует XML + модули в структуре выгрузки конфигурации + регистрирует в Configuration.xml.
Принимает JSON-определение объекта → генерирует XML + модули в структуре выгрузки конфигурации и
регистрирует объект в `Configuration.xml`.
`ConfigDumpInfo.xml` намеренно не трогается: это служебный файл версий объектов, которым управляет
платформа (для инкрементальной выгрузки).
## Порядок работы
1. Составь JSON по синтаксису и примерам ниже → запиши во временный файл
2. Запусти скрипт meta-compile
3. Если нужно изменить созданный объект — `/meta-edit`
4. Если нужно проверить — `/meta-validate`
## Команда
1. Составь JSON по синтаксису ниже → запиши во временный файл.
2. Запусти скрипт.
3. Изменить созданный объект — `/meta-edit`; проверить — `/meta-validate`.
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>"
@@ -28,92 +29,110 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -
| Параметр | Описание |
|----------|----------|
| `JsonPath` | Путь к JSON-файлу (один объект `{...}` или массив `[{...}, ...]`) |
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/` и т.д.) |
| `JsonPath` | Путь к JSON-файлу |
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/`, …) |
## JSON DSL
## Формат JSON
### Общая структура
**Один объект** `{ ... }` или **массив** объектов `[{ ... }, { ... }]` (batch — несколько объектов за прогон).
```json
{ "type": "Catalog", "name": "Номенклатура", ...свойства типа... }
{ "type": "Catalog", "name": "Номенклатура", "...свойства типа...": "..." }
```
`type` и `name` — обязательные. `synonym` генерируется из `name` автоматически (CamelCase → слова через пробел). Можно задать явно: `"synonym": "Мой синоним"`.
`type` и `name` — обязательные. Остальное — по типу (см. индекс ниже). `synonym` по умолчанию выводится из
`name` (CamelCase → слова через пробел); можно задать явно строкой или мультиязычно: `"synonym": { "ru": "…", "en": "…" }`.
### Shorthand реквизитов
## Реквизиты (shorthand)
Используется в `attributes`, `dimensions`, `resources`, `tabularSections`:
Массивы `attributes`, `dimensions`, `resources` и колонки в `tabularSections` задаются строками:
```
"ИмяРеквизита" → String(10) по умолчанию
"ИмяРеквизита: Тип" → с типом
"ИмяРеквизита: Тип | req, index" → с флагами
"Имя" → String(10)
"Имя: Тип" → с типом
"Имя: Тип | req, index" с флагами
```
Типы: `String(100)`, `Number(15,2)`, `Boolean`, `Date`, `DateTime`, `CatalogRef.Xxx`, `DocumentRef.Xxx`, `EnumRef.Xxx`, `DefinedType.Xxx` и др. ссылочные.
**Типы:** `String(100)`, `String(10, fixed)` (фикс. длина), `Number(15,2)`, `Boolean`, `Date`, `DateTime`,
`Time`, ссылочные `CatalogRef.Xxx` / `DocumentRef.Xxx` / `EnumRef.Xxx` / `DefinedType.Xxx` и т.п.
Составной тип — через `+`: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`.
Составной тип: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`.
**Флаги** (после `|`, через запятую):
Флаги: `req`, `index`, `indexAdditional`, `nonneg`, `master`, `mainFilter`, `denyIncomplete`, `useInTotals`.
| Флаг | Значение | Где |
|------|----------|-----|
| `req` | обязательное заполнение | attributes, dimensions, resources |
| `index` | индексировать | attributes, dimensions |
| `indexAdditional` | индекс с доп. упорядочиванием | attributes |
| `multiline` | многострочное поле | attributes |
| `nonneg` | неотрицательное (Number) | attributes, resources |
| `master` | ведущее измерение | dimensions (регистры) |
| `mainFilter` | основной отбор | dimensions (регистры) |
| `denyIncomplete` | запрет незаполненных | dimensions |
| `useInTotals` | использовать в итогах | dimensions (регистр накопления) |
### Свойства по типам
Реквизиту нужны свойства сверх shorthand (значение заполнения, параметры выбора, формат, подсказка, …) —
задаётся **объектной формой**, см. `reference/attributes.md`.
Примеров и shorthand-синтаксиса выше достаточно для типовых задач. Если нужны свойства типа, не показанные в примерах, и их допустимые значения — см. reference-файл:
- `reference/types-basic.md` — Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
- `reference/types-registers.md` — InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
- `reference/types-process.md` — BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal
- `reference/types-web.md` — HTTPService, WebService
Эта инструкция и reference-файлы — полная документация для генерации. Не ищи примеры XML в выгрузках конфигураций.
## Примеры паттернов DSL
### Минимальный объект
## Табличные части
```json
{ "type": "Catalog", "name": "Валюты" }
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] }
```
### С реквизитами
Ключ — имя ТЧ, значение — массив колонок (shorthand) ЛИБО объект со свойствами ТЧ (см. `reference/attributes.md`).
## Индекс: свойства по типам
Для каждого типа — свой reference-файл со свойствами, дефолтами и допустимыми значениями:
| Тип(ы) | Файл |
|--------|------|
| Catalog (справочник) | `reference/catalog.md` |
| Document, DocumentJournal, Sequence, DocumentNumerator | `reference/document.md` |
| InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister | `reference/registers.md` |
| ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes | `reference/charts.md` |
| ExchangePlan | `reference/exchangeplan.md` |
| BusinessProcess, Task | `reference/process.md` |
| Report, DataProcessor | `reference/report-dataprocessor.md` |
| CommonModule, ScheduledJob, EventSubscription | `reference/code.md` |
| HTTPService, WebService | `reference/web.md` |
| Enum, Constant, DefinedType | `reference/simple.md` |
| FunctionalOption, FilterCriterion, SettingsStorage, CommonForm, CommonPicture, CommonTemplate, служебные | `reference/other-types.md` |
Кросс-типовые детали:
- **`reference/attributes.md`** — объектная форма реквизита и колонки ТЧ (значение заполнения, параметры
выбора, формат, подсказка, границы, …) + свойства самой ТЧ.
- **`reference/blocks.md`** — блоки объекта: представления, команды (+ характеристики/стандартные реквизиты).
Эта инструкция и reference-файлы — полная документация. Не ищи примеры XML в выгрузках конфигураций.
## Примеры
Справочник с реквизитами:
```json
{
"type": "Catalog", "name": "Организации",
"descriptionLength": 100,
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"]
}
{ "type": "Catalog", "name": "Организации", "descriptionLength": 100,
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"] }
```
### С табличной частью
Документ с движениями и ТЧ:
```json
{
"type": "Document", "name": "ПриходнаяНакладная",
{ "type": "Document", "name": "ПриходнаяНакладная",
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] }
}
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] } }
```
### Регистровый паттерн (измерения + ресурсы)
Регистр сведений:
```json
{
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
{ "type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"]
}
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] }
```
### Batch — несколько объектов в одном файле
Batch:
```json
[
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
[ { "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
{ "type": "Catalog", "name": "Валюты" },
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
]
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" } ]
```
@@ -0,0 +1,140 @@
# Объектная форма реквизита и табличной части
Когда реквизиту (в `attributes` / `dimensions` / `resources` / колонках ТЧ) нужны свойства сверх
shorthand — вместо строки задаётся объект:
```json
{ "name": "Цена", "type": "Number(15,2)", "tooltip": "Цена за единицу", "fillValue": 0 }
```
`name` и `type` обязательны (тип можно задать и раздельно: `"type": "Number", "length": 15, "precision": 2`).
Остальные ключи — ниже, все со значением по умолчанию (не задавать, если устраивает дефолт).
## Свойства реквизита
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `synonym` | из имени | ML (строка или `{ru,en}`) |
| `tooltip` | пусто | ML |
| `comment` | пусто | строка |
| `fillChecking` | `DontCheck` | `DontCheck` / `ShowError` / `ShowWarning` (то же, что флаг `req`) |
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
| `fillFromFillingValue` | `false` | bool |
| `fillValue` | по типу (см. ниже) | значение заполнения |
| `createOnInput` | `Auto` | `Auto` / `Use` / `DontUse` |
| `quickChoice` | `Auto` | `Auto` / `Use` / `DontUse` |
| `choiceHistoryOnInput` | `Auto` | `Auto` / `DontUse` |
| `dataHistory` | `Use` | `Use` / `DontUse` |
| `use` | `ForItem` | `ForItem` / `ForFolder` / `ForFolderAndItem` (реквизит иерархического справочника) |
| `passwordMode` | `false` | bool |
| `multiLine` | `false` | bool (то же, что флаг `multiline`) |
| `extendedEdit` | `false` | bool (расширенное редактирование — многострочный ввод) |
| `mask` | пусто | строка маски ввода |
| `format` / `editFormat` | пусто | форматная строка 1С (ML) |
| `markNegatives` | `false` | bool (выделять отрицательные, для Number) |
| `minValue` / `maxValue` | не задано | граница диапазона (см. ниже) |
| `choiceParameterLinks` | пусто | связи параметров выбора (см. ниже) |
| `choiceParameters` | пусто | параметры выбора (см. ниже) |
| `choiceForm` | пусто | ссылка на форму выбора `Тип.Объект.Form.ИмяФормы` |
| `choiceFoldersAndItems` | `Items` | `Items` / `Folders` / `FoldersAndItems` (что выбирать в иерарх. справочнике) |
Индексирование задаётся флагом `index` / `indexAdditional` в shorthand, либо в объекте — как и в строковой форме,
через `"type": "… | index"`.
### `fillValue` — значение заполнения
Пустое значение по типу компилятор подставляет сам — ключ **не задают**:
| Тип реквизита | Пустое значение |
|---------------|-----------------|
| String | пустая строка |
| Number | `0` |
| Boolean, Date, ссылочный, составной | не задано (nil) |
Ключ `fillValue` задают для **конкретного** значения — интерпретируется по типу реквизита:
- **Boolean**`true` / `false`.
- **Number** — число (`21`, `1.5`).
- **String** — строка.
- **Date** — ISO-строка `"2020-01-01T00:00:00"`.
- **Ссылочный** — путь: `"Catalog.Валюты.EmptyRef"` (пустая ссылка), `"Enum.Периодичность.EnumValue.Месяц"`
(значение перечисления), `"Catalog.СтраныМира.Россия"` (предопределённый элемент).
- **`null`** — явно «значение не задано» (nil), когда нужно перекрыть непустой дефолт типа.
- **`{ "emptyRef": true }`** — пустая ссылка для реквизита типа `DefinedType.X` (когда тип из пути не выводится).
> Пустая ссылка (`EmptyRef`) и `null` — разное: платформа хранит их отдельно.
### `minValue` / `maxValue` — границы диапазона
Число → числовая граница; строка → строковая (напр. год `"2000"`). Без ключа — граница не задана.
### `choiceParameterLinks` — связи параметров выбора
Связывают параметр выбора этого реквизита с другим реквизитом объекта. Массив строк или объектов:
```json
"choiceParameterLinks": ["Отбор.Организация=Организация", "Отбор.Договор=Договор:DontChange"]
"choiceParameterLinks": [{ "name": "Отбор.Организация", "dataPath": "Организация", "valueChange": "Clear" }]
```
- `dataPath` — реквизит **того же объекта**: имя обычного реквизита (`"Организация"`) или стандартного
(`"Владелец"`, `"Ссылка"`).
- `valueChange``Clear` (по умолчанию) / `DontChange`.
### `choiceParameters` — параметры выбора
Фиксируют параметр выбора значением. Массив строк или объектов:
```json
"choiceParameters": ["Отбор.ЭтоГруппа=false"]
"choiceParameters": [{ "name": "Отбор.Владелец", "value": "Catalog.Организации.EmptyRef" }]
```
- `value` — bool / число / строка / ссылочный путь (несёт тип) ИЛИ массив (список фиксированных значений).
- Для набора голых имён-значений добавьте `type` (тип поля-фильтра), чтобы они стали ссылками:
`{ "name": "Отбор.Тип", "type": "EnumRef.ТипыВЕТИС", "value": ["EmptyRef", "ТТН"] }`.
### Редкие ключи
`linkByType` — связь по типу (тип реквизита-Характеристики берётся из другого реквизита):
`{ "dataPath": "Свойство", "linkItem": 0 }` или строка-путь. Применяется для реквизитов-характеристик.
---
## Табличная часть — объектная форма
Значение в `tabularSections` — массив колонок ЛИБО объект со свойствами самой ТЧ:
```json
"tabularSections": {
"Товары": {
"synonym": { "ru": "Товары", "en": "Goods" },
"tooltip": "Строки заказа",
"fillChecking": "ShowError",
"attributes": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"]
}
}
```
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `synonym` | из имени | ML |
| `tooltip` | пусто | ML |
| `comment` | пусто | строка |
| `fillChecking` | `DontCheck` | `DontCheck` / `ShowError` / `ShowWarning` (обязательность заполнения ТЧ) |
| `use` | `ForItem` | `ForItem` / `ForFolder` / `ForFolderAndItem` (только Catalog / ChartOfCharacteristicTypes) |
| `attributes` | `[]` | колонки (shorthand или объектная форма реквизита) |
| `lineNumber` | — | кастомизация стандартного реквизита НомерСтроки (см. ниже) |
| `lineNumberLength` | по режиму совместимости | `5``9` — разрядность номера строки: `5` → до 99 999 строк, `9` → до 999 999 999. Требует формата 2.20 (платформа 8.3.27) |
### `lineNumber` — стандартный реквизит НомерСтроки
У каждой ТЧ есть стандартный реквизит НомерСтроки. По умолчанию все его свойства типовые. Ключ `lineNumber`
на объектной форме ТЧ их переопределяет:
```json
"Строки": { "lineNumber": { "synonym": "Номер п/п", "fullTextSearch": "DontUse" }, "attributes": [...] }
```
Переопределяемые: `synonym`, `comment`, `fullTextSearch` (`Use`/`DontUse`), `tooltip`, `format`, `editFormat`,
`choiceHistoryOnInput` (`Auto`/`DontUse`).
@@ -0,0 +1,109 @@
# Блоки объекта
Кросс-типовые блоки уровня объекта (применимы к ссылочным типам — Catalog, Document, ChartOf*, ExchangePlan,
BusinessProcess, Task и др.).
## Представления
Тексты представления объекта в интерфейсе (ML — строка или `{ru,en}`, по умолчанию пусто):
| Ключ | Смысл |
|------|-------|
| `objectPresentation` | представление объекта |
| `extendedObjectPresentation` | расширенное представление объекта |
| `listPresentation` | представление списка |
| `extendedListPresentation` | расширенное представление списка |
| `explanation` | пояснение |
Набор доступных ключей зависит от типа (у списочных без формы объекта нет `objectPresentation` и т.п.).
```json
"listPresentation": "Организации", "objectPresentation": { "ru": "Организация", "en": "Company" }
```
## Команды
Команды объекта. Ключ — имя команды, значение — объект свойств (map `имя → объект` или массив `[{name, …}]`).
Для каждой команды создаётся заготовка модуля с обработчиком `ОбработкаКоманды`.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `synonym` | из имени | ML |
| `tooltip` | пусто | ML |
| `comment` | пусто | строка |
| `group` | **обязательно** | группа размещения (см. ниже) |
| `commandParameterType` | пусто | тип параметра (напр. `CatalogRef.Номенклатура`) — **только для групп формы** |
| `parameterUseMode` | `Single` | `Single` / `Multiple` |
| `modifiesData` | `false` | bool |
| `representation` | `Auto` | вид отображения |
| `picture` | пусто | ссылка на картинку (`StdPicture.Print`, `CommonPicture.Загрузка`) |
| `shortcut` | пусто | сочетание клавиш |
```json
"commands": {
"ПечатьЭтикеток": { "synonym": "Печать этикеток", "group": "FormCommandBarImportant",
"commandParameterType": "CatalogRef.Номенклатура", "picture": "StdPicture.Print" }
}
```
**Группа (`group`) обязательна** — каждая команда размещается в группе командного интерфейса:
- **Командный интерфейс раздела** (панель навигации / панель действий; `commandParameterType` **недоступен**):
`NavigationPanelImportant` / `NavigationPanelOrdinary` / `NavigationPanelSeeAlso`,
`ActionsPanelCreate` / `ActionsPanelReports` / `ActionsPanelTools`.
- **Командный интерфейс формы** (`commandParameterType` допустим): `FormCommandBarImportant` /
`FormCommandBarCreateBasedOn`, `FormNavigationPanelImportant` / `FormNavigationPanelGoTo` / `FormNavigationPanelSeeAlso`.
- **Кастомная группа:** `CommandGroup.<Имя>` (параметр допустим).
Группа раздела вместе с `commandParameterType` → ошибка.
## `inputByString` / `dataLockFields` / `basedOn`
Списки полей/объектов уровня объекта. Поля — по имени реквизита объекта (обычного или стандартного).
- **`inputByString`** — поля быстрого ввода по строке. По умолчанию выводятся из Кода/Наименования — ключ не нужен;
задать при другом наборе/порядке, либо `[]` для отключения.
```json
"inputByString": ["Код", "Наименование", "Контрагент"]
```
- **`dataLockFields`** — поля управляемой блокировки данных (по умолчанию пусто).
```json
"dataLockFields": ["Организация", "Контрагент"]
```
- **`basedOn`** — «ввод на основании»: список ссылок на объекты метаданных (по умолчанию пусто).
```json
"basedOn": ["Catalog.Контрагенты", "Document.ЗаказПоставщику"]
```
## `standardAttributes` — кастомизация стандартных реквизитов
Стандартные реквизиты объекта (Наименование, Код, Владелец, …) переопределяются блоком
`standardAttributes` — объект `{ ИмяРеквизита: { переопределения } }`. Имена — как в 1С: `Description`, `Code`,
`Owner`, `Parent`, `DeletionMark`, `Ref` и т.д. (для Document — `Date`, `Number`, `Posted`).
Переопределяемые поля — как у обычного реквизита (`synonym`, `tooltip`, `fillChecking`, `fillValue`,
`choiceParameters`, `comment`, `mask`, `choiceForm`; полный набор — `attributes.md`).
```json
"standardAttributes": {
"Description": { "synonym": "Наименование контрагента" },
"Code": { "fillChecking": "ShowError" }
}
```
## `characteristics` — «Дополнительные реквизиты и сведения»
Привязка плана видов характеристик. Массив; каждый элемент связывает **источник типов** (где определены
характеристики) и **источник значений** (где хранятся значения).
```json
"characteristics": [{
"types": { "from": "Catalog.НаборыДопРеквизитов.ДополнительныеРеквизиты",
"key": "Свойство", "filterField": "Ссылка", "filterValue": "Справочник_Организации" },
"values": { "from": "Catalog.Организации.TabularSection.ДополнительныеРеквизиты",
"object": "Ссылка", "type": "Свойство", "value": "Значение" }
}]
```
- `from` — таблица-источник; `key`/`filterField`/`object`/`type`/`value` — поля источника (по имени реквизита).
- `filterValue` — значение фильтра типов: имя предопределённого набора (строка) или путь к элементу.
@@ -0,0 +1,71 @@
# Catalog (Справочник)
```json
{ "type": "Catalog", "name": "Организации", "descriptionLength": 100,
"attributes": ["ИНН: String(12)", "КПП: String(9)"] }
```
## Свойства
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `hierarchical` | `false` | bool |
| `hierarchyType` | `HierarchyFoldersAndItems` | `HierarchyFoldersAndItems` / `HierarchyOfItems` |
| `limitLevelCount` | `false` | bool (ограничивать кол-во уровней) |
| `levelCount` | `2` | число уровней (при `limitLevelCount`) |
| `foldersOnTop` | `true` | bool (группы сверху) |
| `owners` | `[]` | массив владельцев: `["Catalog.Контрагенты"]` |
| `subordinationUse` | `ToItems` | `ToItems` / `ToFolders` / `ToFoldersAndItems` (кому подчинён) |
| `codeLength` | `9` | длина кода (0 — без кода) |
| `codeType` | `String` | `String` / `Number` |
| `codeAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `codeSeries` | `WholeCatalog` | `WholeCatalog` / `WithinSubordination` / `WithinOwnerSubordination` |
| `autonumbering` | `true` | bool (автонумерация) |
| `checkUnique` | `false` | bool (контроль уникальности кода) |
| `descriptionLength` | `25` | длина наименования |
| `defaultPresentation` | `AsDescription` | `AsDescription` / `AsCode` |
| `quickChoice` | `true` | bool (быстрый выбор) |
| `choiceMode` | `BothWays` | `BothWays` / `QuickChoice` / `FromForm` |
| `editType` | `InDialog` | `InDialog` / `InList` / `BothWays` |
| `createOnInput` | `Use` | `Auto` / `Use` / `DontUse` |
| `choiceHistoryOnInput` | `Auto` | `Auto` / `DontUse` |
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
| `fullTextSearchOnInputByString` | `DontUse` | `Use` / `DontUse` |
| `searchStringModeOnInputByString` | `Begin` | `Begin` / `AnyPart` |
| `predefinedDataUpdate` | `Auto` | `Auto` / `DontAutoUpdate` / `AutoUpdate` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `useStandardCommands` | `true` | bool |
| `includeHelpInContents` | `false` | bool |
| `attributes` | `[]` | реквизиты (shorthand / объектная форма) |
| `tabularSections` | `{}` | табличные части |
**Формы.** Ссылка на форму — `Тип.Объект.Form.ИмяФормы` (напр. `Catalog.Организации.Form.ФормаЭлемента`).
Слоты основных форм: `defaultObjectForm`, `defaultFolderForm`, `defaultListForm`, `defaultChoiceForm`,
`defaultFolderChoiceForm`; вспомогательных — те же имена с префиксом `auxiliary` (`auxiliaryObjectForm`, …).
## `predefined` — предопределённые элементы
Массив предопределённых элементов → `Ext/Predefined.xml`. Элемент — строка (плоский случай) или объект (иерархия).
**Строка:** `"(Код) Имя [Наименование]"``Имя` обязательно; `(Код)` и `[Наименование]` опциональны.
Без `[...]` наименование выводится из имени; `[]` — пустое; `[текст]` — заданное.
```json
"predefined": [
"Основной",
"(1) ДокументОПриемке [Документ о приемке]",
{ "name": "Группа1", "isFolder": true, "description": "Прочие",
"childItems": ["Факс", "(7) Скайп"] }
]
```
**Объект:** `name` (обязательно), `code`, `description` (наименование), `isFolder` (признак группы),
`childItems` (вложенные, рекурсивно). Тип кода — по свойству `codeType`.
## Дополнительно
- Свойства реквизитов и табличных частей — `attributes.md`.
- Представления (`objectPresentation`, `listPresentation`, …), команды объекта, характеристики
(«ДопРеквизиты и сведения»), кастомизация стандартных реквизитов, `inputByString` / `dataLockFields` /
`basedOn``blocks.md`.
@@ -0,0 +1,105 @@
# Планы: ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes
Все три — ссылочные типы (наследуют слой Catalog: коды, `standardAttributes`, `characteristics`, `inputByString`,
формы, представления — см. `catalog.md` / `attributes.md` / `blocks.md`) с предопределёнными элементами и своими
специальными свойствами.
## ChartOfCharacteristicTypes (План видов характеристик)
Хранит определения характеристик (видов). Иерархический (папки+элементы).
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `valueType` | любой примитив | тип значения характеристики (составной — строка `"A + B"` или массив `valueTypes`) |
| `characteristicExtValues` | пусто | ссылка на справочник доп. значений |
| `hierarchical` | `false` | bool |
| `foldersOnTop` | `true` | bool |
| `codeLength` | `9` | длина кода |
| `descriptionLength` | `100` | длина наименования |
| `checkUnique` | `true` | bool |
| `autonumbering` | `true` | bool |
| `codeSeries` | `WholeCharacteristicKind` | серия кодов |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `predefined` | `[]` | предопределённые виды (несут тип значения — см. ниже) |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
**Предопределённые виды** несут **тип значения на элемент** — короткой строкой после `:`
(`"(Код) Имя [Наименование]: Тип"`, составной через `+`) или объектной формой с ключом `type`:
```json
"predefined": [
"(000001) Цвет: CatalogRef.Цвета",
"(000002) Размер [Размер одежды]: String(50) + Number(3,0)",
{ "name": "Группа", "isFolder": true, "type": "" }
]
```
## ChartOfAccounts (План счетов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `extDimensionTypes` | пусто | ссылка на ПВХ видов субконто `ChartOfCharacteristicTypes.X` |
| `maxExtDimensionCount` | `0` (без ПВХ) / `3` (с ПВХ) | макс. число субконто |
| `codeMask` | пусто | маска кода счёта (напр. `"@@@.@@"`) |
| `codeLength` | `9` | длина кода |
| `descriptionLength` | `25` | длина наименования |
| `checkUnique` | `true` | bool |
| `codeSeries` | `WholeChartOfAccounts` | серия кодов |
| `defaultPresentation` | `AsCode` | `AsCode` / `AsDescription` |
| `autoOrderByCode` | `true` | bool |
| `orderLength` | `9` | длина строки упорядочивания |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `accountingFlags` | `[]` | признаки учёта (как реквизиты, тип по умолчанию Boolean; массив имён/реквизитов) |
| `extDimensionAccountingFlags` | `[]` | признаки учёта субконто (как реквизиты) |
| `predefined` | `[]` | предопределённые счета (см. ниже) |
**Предопределённый счёт** (объектная форма):
| Поле | Умолчание | Значения |
|------|-----------|----------|
| `name` | — | имя (обязательно) |
| `code` | пусто | код счёта |
| `description` | из имени | наименование |
| `accountType` | `ActivePassive` | `Active` / `Passive` / `ActivePassive` |
| `offBalance` | `false` | bool (забалансовый) |
| `order` | — | строка сортировки |
| `flags` | `[]` | включённые признаки учёта (только TRUE) |
| `subconto` | `[]` | виды субконто (см. ниже) |
| `childItems` | `[]` | подчинённые счета |
`subconto` — строка `"Вид | Признак1, Признак2"` (после `|` — включённые признаки учёта субконто; токен `Turnover`
«только обороты») или объект `{ type, turnover, flags }`. `Вид` — имя предопределённого вида из ПВХ `extDimensionTypes`.
```json
"predefined": [
{ "name": "ОсновныеСредства", "code": "01", "accountType": "Active", "order": " 01",
"flags": ["Количественный"], "subconto": ["Номенклатура | Суммовой, Валютный"],
"childItems": [ { "name": "ОСВОрганизации", "code": "01.01", "accountType": "Active", "order": " 01.01" } ] }
]
```
## ChartOfCalculationTypes (План видов расчёта)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `codeLength` | `5` | длина кода |
| `descriptionLength` | `100` | длина наименования |
| `codeAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `dependenceOnCalculationTypes` | `DontUse` | `DontUse` / `OnPeriod` / `OnActionPeriod` |
| `baseCalculationTypes` | `[]` | базовые виды расчёта (список ссылок `ChartOfCalculationTypes.X`) |
| `actionPeriodUse` | `false` | bool (использовать период действия) |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `predefined` | `[]` | предопределённые виды расчёта (см. ниже) |
**Предопределённый вид расчёта** — плоский: строка `"(Код) Имя [Наименование]"` или объект
`{ name, code, description, actionPeriodIsBase }` (`actionPeriodIsBase` — bool, по умолчанию `false`).
```json
"predefined": [ "(00001) Оклад [Оклад по дням]", { "name": "Премия", "code": "00002", "actionPeriodIsBase": true } ]
```
> **ChartOfAccounts** ссылается на ПВХ через `extDimensionTypes`. Регистр бухгалтерии/расчёта требует
> соответствующий план (см. `registers.md`).
@@ -0,0 +1,56 @@
# CommonModule, ScheduledJob, EventSubscription (объекты, привязанные к коду)
## CommonModule (Общий модуль)
Флаги контекста выполнения (все bool, по умолчанию `false`). Создаёт пустой `Ext/Module.bsl`.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `context` | — | шорткат флагов (см. ниже) |
| `global` | `false` | bool |
| `server` | `false` | bool |
| `serverCall` | `false` | bool (вызов сервера) |
| `clientManagedApplication` | `false` | bool (клиент управляемого приложения) |
| `clientOrdinaryApplication` | `false` | bool (клиент обычного приложения) |
| `externalConnection` | `false` | bool |
| `privileged` | `false` | bool |
| `returnValuesReuse` | `DontUse` | `DontUse` / `DuringRequest` / `DuringSession` |
Шорткат `context`: `"server"` → Server+ServerCall; `"client"` → ClientManagedApplication;
`"serverClient"` → Server+ClientManagedApplication.
```json
{ "type": "CommonModule", "name": "ОбменДаннымиСервер", "context": "server", "returnValuesReuse": "DuringRequest" }
```
## ScheduledJob (Регламентное задание)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `methodName` | пусто | метод-обработчик `"МодульСервер.Процедура"` (дополняется до `CommonModule.…`) |
| `description` | пусто | наименование задания |
| `key` | пусто | ключ |
| `use` | `false` | bool (использование) |
| `predefined` | `false` | bool (предопределённое) |
| `restartCountOnFailure` | `3` | число повторов при сбое |
| `restartIntervalOnFailure` | `10` | интервал повтора, сек |
```json
{ "type": "ScheduledJob", "name": "ОбменДанными", "methodName": "ОбменДаннымиСервер.Выполнить", "use": true }
```
## EventSubscription (Подписка на событие)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `source` | `[]` | объекты-источники: `["CatalogObject.Контрагенты", "DocumentObject.Реализация"]` |
| `event` | `BeforeWrite` | `BeforeWrite` / `OnWrite` / `BeforeDelete` / `OnReadAtServer` / `FillCheckProcessing` … |
| `handler` | пусто | метод-обработчик `"МодульСервер.Процедура"` (дополняется до `CommonModule.…`) |
```json
{ "type": "EventSubscription", "name": "ПередЗаписьюКонтрагента",
"source": ["CatalogObject.Контрагенты"], "event": "BeforeWrite",
"handler": "ОбщегоНазначенияСервер.ПередЗаписьюКонтрагента" }
```
> Процедура-обработчик (`methodName` / `handler`) должна существовать в указанном общем модуле (экспортная).
@@ -0,0 +1,79 @@
# Document, DocumentJournal, Sequence, DocumentNumerator
## Document (Документ)
```json
{ "type": "Document", "name": "ПриходнаяНакладная",
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
"attributes": ["Организация: CatalogRef.Организации"],
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] } }
```
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `numerator` | пусто | ссылка на нумератор `DocumentNumerator.X` |
| `numberType` | `String` | `String` / `Number` |
| `numberLength` | `11` | длина номера |
| `numberAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `numberPeriodicity` | `Year` | `Nonperiodical` / `Day` / `Month` / `Quarter` / `Year` |
| `checkUnique` | `true` | bool |
| `autonumbering` | `true` | bool |
| `posting` | `Allow` | `Allow` / `Deny` (проведение) |
| `realTimePosting` | `Deny` | `Allow` / `Deny` (оперативное проведение) |
| `registerRecordsDeletion` | `AutoDelete` | `AutoDelete` / `AutoDeleteOnUnpost` / `AutoDeleteOff` |
| `registerRecordsWritingOnPost` | `WriteSelected` | `WriteModified` / `WriteSelected` / `WriteAll` |
| `sequenceFilling` | `AutoFill` | заполнение последовательностей |
| `postInPrivilegedMode` | `true` | bool |
| `unpostInPrivilegedMode` | `true` | bool |
| `createOnInput` | `Use` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
| `dataHistory` | `DontUse` | `Use` / `DontUse` |
| `registerRecords` | `[]` | движения: список ссылок `["AccumulationRegister.ОстаткиТоваров", "InformationRegister.Цены"]` |
| `useStandardCommands` | `true` | bool |
| `includeHelpInContents` | `false` | bool |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
Формы: `defaultObjectForm`, `defaultListForm`, `defaultChoiceForm`, `auxiliary*` (см. `catalog.md`).
Реквизиты и ТЧ — `attributes.md`. Представления, команды, характеристики, `basedOn`, `standardAttributes`,
`inputByString`, `dataLockFields``blocks.md`.
## DocumentJournal (Журнал документов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `registeredDocuments` | `[]` | документы журнала: `["Document.Встреча", "Document.Звонок"]` |
| `columns` | `[]` | графы журнала (см. ниже) |
Графа — строка `"Имя"` или объект `{ name, synonym, indexing, references }`, где `indexing``Index`/`DontIndex`,
`references` — пути к реквизитам документов, отображаемым в графе.
```json
{ "type": "DocumentJournal", "name": "Взаимодействия",
"registeredDocuments": ["Document.Встреча", "Document.Звонок"],
"columns": [{ "name": "Организация", "indexing": "Index",
"references": ["Document.Встреча.Attribute.Организация"] }] }
```
## Sequence (Последовательность документов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `moveBoundaryOnPosting` | `DontMove` | сдвиг границы при проведении |
| `documents` | `[]` | документы последовательности (список ссылок) |
| `registerRecords` | `[]` | движения (список ссылок) |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dimensions` | `[]` | измерения `{name, type, documentMap[], registerRecordsMap[]}` |
`documentMap` / `registerRecordsMap` — пути к реквизитам документов / движениям, соответствующим измерению.
## DocumentNumerator (Нумератор документов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `numberType` | `String` | `String` / `Number` |
| `numberLength` | `11` | длина номера |
| `numberAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `numberPeriodicity` | `Year` | `Nonperiodical` / `Day` / … / `Year` |
| `checkUnique` | `true` | bool |
@@ -0,0 +1,41 @@
# ExchangePlan (План обмена)
Близок к справочнику (без иерархии/владельцев), плюс состав объектов обмена. Наследует слой Catalog:
`codeLength`, `codeAllowedLength`, `descriptionLength`, `defaultPresentation`, `editType`, `quickChoice`,
`choiceMode`, формы, `standardAttributes`, `characteristics`, `inputByString`, `basedOn`, представления —
см. `catalog.md` / `attributes.md` / `blocks.md`.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `distributedInfoBase` | `false` | bool (распределённая ИБ — РИБ) |
| `includeConfigurationExtensions` | `false` | bool (включать расширения конфигурации) |
| `descriptionLength` | `150` | длина наименования |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dataHistory` | `DontUse` | `Use` / `DontUse` |
| `useStandardCommands` | `true` | bool |
| `content` | `[]` | состав обмена (см. ниже) |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
## `content` — состав плана обмена
Список объектов-участников обмена; у каждого — признак авторегистрации изменений (по умолчанию выключена).
Элемент — ссылка на объект метаданных (строка) или объект с признаком:
```json
"content": [
"Catalog.Организации", // авторегистрация выключена
"InformationRegister.Курсы: autoRecord", // авторегистрация включена (токен)
{ "metadata": "Document.РеализацияТоваров", "autoRecord": true }
]
```
- Строка `"Тип.Имя"` — авторегистрация выключена; суффикс `: autoRecord` — включена.
- Объект: `metadata` (ссылка), `autoRecord` (bool или `Allow`/`Deny`).
```json
{ "type": "ExchangePlan", "name": "ОбменССайтом", "distributedInfoBase": false,
"content": ["Catalog.Номенклатура: autoRecord", "Catalog.Контрагенты: autoRecord"],
"attributes": ["АдресСервера: String(200)"] }
```
@@ -0,0 +1,90 @@
# Прочие типы
Редкие/служебные объекты. Каждый — минимальный набор свойств.
## FunctionalOption (Функциональная опция)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `location` | пусто | где хранится значение: `Constant.X` / `InformationRegister.X.Resource.Y` / `<Тип>.X.Attribute.Y` |
| `content` | `[]` | реквизиты/измерения/ресурсы, зависящие от опции (полные пути к объектам) |
| `privilegedGetMode` | `true` | bool |
| `comment` | пусто | строка |
```json
{ "type": "FunctionalOption", "name": "ВестиУчетПоСкладам", "location": "Constant.ВестиУчетПоСкладам",
"content": ["Document.РеализацияТоваров.TabularSection.Товары.Attribute.Склад"] }
```
## FilterCriterion (Критерий отбора)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `valueType` | — | тип значения отбора (составной через `+`) |
| `content` | `[]` | реквизиты, по которым идёт отбор (пути к объектам) |
| `useStandardCommands` | `true` | bool |
| `defaultForm` / `auxiliaryForm` | пусто | формы |
| `comment` | пусто | строка |
```json
{ "type": "FilterCriterion", "name": "ДокументыПоКонтрагенту", "valueType": "CatalogRef.Контрагенты",
"content": ["Document.Реализация.Attribute.Контрагент"] }
```
## SettingsStorage (Хранилище настроек)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `defaultSaveForm` / `defaultLoadForm` | пусто | формы сохранения / загрузки |
| `auxiliarySaveForm` / `auxiliaryLoadForm` | пусто | вспомогательные формы |
| `comment` | пусто | строка |
## CommonForm (Общая форма)
Создаёт метаданные + заготовку формы. Содержимое формы наполняется `/form-compile` или `/form-edit`.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `formType` | `Managed` | тип формы |
| `usePurposes` | `[PlatformApplication, MobilePlatformApplication]` | назначение (массив) |
| `useStandardCommands` | `false` | bool |
| `includeHelpInContents` | `false` | bool |
| `comment` | пусто | строка |
```json
{ "type": "CommonForm", "name": "НастройкиОбмена", "usePurposes": ["PlatformApplication"] }
```
## CommonPicture / CommonTemplate (Общие картинки и макеты)
Только метаданные + регистрация; содержимое (`Ext/Picture*`, `Ext/Template.*`) импортируется отдельно
(для табличного макета — `/mxl-compile`).
- **CommonPicture**`availabilityForChoice` / `availabilityForAppearance` (bool, по умолчанию `false`).
- **CommonTemplate**`templateType` (`SpreadsheetDocument` по умолчанию / `TextDocument` / `HTMLDocument` /
`BinaryData` / `AddIn` / `DataCompositionSchema` / `DataCompositionAppearanceTemplate` / `GraphicalSchema`).
```json
{ "type": "CommonTemplate", "name": "ПечатьЗаказа", "templateType": "SpreadsheetDocument" }
```
## Служебные типы
- **SessionParameter** (параметр сеанса) — `valueType` (тип значения, составной через `+`).
- **FunctionalOptionsParameter** (параметр функциональной опции) — `use` (массив измерений/реквизитов).
- **WSReference** (WS-ссылка) — `locationURL` (URL WSDL).
- **CommandGroup** (группа команд) — `category` (по умолч. `NavigationPanel`) — где размещается группа:
`NavigationPanel` / `ActionsPanel` (командный интерфейс раздела) или `FormCommandBar` / `FormNavigationPanel`
(командный интерфейс формы); `representation` (`Auto`), `tooltip` (ML), `picture`. Команды объекта ссылаются на
группу через `group: "CommandGroup.<Имя>"` (см. `blocks.md`).
- **CommonCommand** (общая команда) — `group`, `representation`, `tooltip`, `picture`, `shortcut`,
`commandParameterType`, `parameterUseMode` (`Single`/`Multiple`), `modifiesData`, `includeHelpInContents`.
Создаёт `Ext/CommandModule.bsl`.
- **CommonAttribute** (общий реквизит) — `valueType` (по умолчанию `String(0)`) + свойства реквизита
(`attributes.md`) + `content` (объекты, куда входит реквизит) + свойства разделения данных
(`dataSeparation`, `separatedDataUse`, `usersSeparation`, … — по умолчанию `DontUse`/`Independently`).
```json
{ "type": "CommonAttribute", "name": "Организация", "valueType": "CatalogRef.Организации",
"autoUse": "Use", "content": ["Document.РеализацияТоваров", "Document.ПоступлениеТоваров"] }
```

Some files were not shown because too many files have changed in this diff Show More