Compare commits

..
219 Commits
Author SHA1 Message Date
Nick ShirokovandClaude Opus 4.8 e01688e764 fix(form-add): идемпотентная регистрация <Form>/<Template> в ChildObjects
form-add и template-add вставляли запись в ChildObjects безусловно.
Если форма/макет уже зарегистрированы (например, form-compile
регистрирует <Form>, не создавая файл метаданных, а затем вызывается
form-add) — возникал дубль <Form>/<Template>, ломавший валидацию.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Что сделано:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #32, #33

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

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

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

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

Fixes #28

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

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

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

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

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

Fixes #26, #27

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 15:18:41 +03:00
1206 changed files with 142224 additions and 104195 deletions
+17 -3
View File
@@ -1,4 +1,4 @@
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml) # cf-edit v1.10 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath, [Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
@@ -44,6 +44,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null 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) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -80,10 +90,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} 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 $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { 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 $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -196,7 +209,7 @@ Info "Configuration: $($script:objName)"
$script:typeOrder = @( $script:typeOrder = @(
"Language","Subsystem","StyleItem","Style", "Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate", "CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", "FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference", "XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -212,7 +225,7 @@ $script:typeOrder = @(
$script:typeToDir = @{ $script:typeToDir = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates" "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans" "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences" "XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions" "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups" "FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
@@ -700,6 +713,7 @@ $script:ruTypeMap = @{
"регистррасчёта" = "CalculationRegister" "регистррасчёта" = "CalculationRegister"
"бизнеспроцесс" = "BusinessProcess" "бизнеспроцесс" = "BusinessProcess"
"задача" = "Task" "задача" = "Task"
"бот" = "Bot"
"планобмена" = "ExchangePlan" "планобмена" = "ExchangePlan"
"хранилищенастроек" = "SettingsStorage" "хранилищенастроек" = "SettingsStorage"
} }
+62 -8
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml) # cf-edit v1.10 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -33,6 +33,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -72,6 +84,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) 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) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -79,6 +94,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
@@ -182,7 +199,7 @@ XS_NS = "http://www.w3.org/2001/XMLSchema"
TYPE_ORDER = [ TYPE_ORDER = [
"Language", "Subsystem", "StyleItem", "Style", "Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate", "CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan", "FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference", "XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption", "EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup", "FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
@@ -198,7 +215,7 @@ TYPE_ORDER = [
TYPE_TO_DIR = { TYPE_TO_DIR = {
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles", "Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates", "CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans", "FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences", "XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions", "EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups", "FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
@@ -307,13 +324,49 @@ def parse_batch_value(val):
return items return items
def save_xml_bom(tree, path): def _detect_xml_style(path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>') финальный перенос. None → файл новый (сохранить текущее поведение)."""
if not xml_bytes.endswith(b"\n"): try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_bom(tree, path):
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf") if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes) f.write(xml_bytes)
@@ -771,6 +824,7 @@ def main():
"регистррасчета": "CalculationRegister", "регистррасчета": "CalculationRegister",
"регистррасчёта": "CalculationRegister", "регистррасчёта": "CalculationRegister",
"бизнеспроцесс": "BusinessProcess", "бизнеспроцесс": "BusinessProcess",
"бот": "Bot",
"задача": "Task", "планобмена": "ExchangePlan", "задача": "Task", "планобмена": "ExchangePlan",
"хранилищенастроек": "SettingsStorage", "хранилищенастроек": "SettingsStorage",
} }
+3 -2
View File
@@ -1,4 +1,4 @@
# cf-info v1.3 — Compact summary of 1C configuration root # cf-info v1.4 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath, [Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
@@ -89,7 +89,7 @@ function Get-PropML([string]$propName) {
$typeOrder = @( $typeOrder = @(
"Language","Subsystem","StyleItem","Style", "Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate", "CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", "FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference", "XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -105,6 +105,7 @@ $typeRuNames = @{
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили" "Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли" "CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули" "CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
"Bot"="Боты"
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты" "CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки" "WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания" "EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
+3 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-info v1.3 — Compact summary of 1C configuration root # cf-info v1.4 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -95,7 +95,7 @@ def get_prop_ml(prop_name):
type_order = [ type_order = [
"Language", "Subsystem", "StyleItem", "Style", "Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate", "CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan", "FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference", "XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption", "EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup", "FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
@@ -111,6 +111,7 @@ type_ru_names = {
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили", "Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли", "CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули", "CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
"Bot": "Боты",
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты", "CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки", "WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания", "EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
@@ -1,4 +1,4 @@
# cf-validate v1.3 — Validate 1C configuration root structure # cf-validate v1.4 — Validate 1C configuration root structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -104,11 +104,11 @@ $validClassIds = @(
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface "fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
) )
# 44 types in canonical order # 45 types in canonical order
$childObjectTypes = @( $childObjectTypes = @(
"Language","Subsystem","StyleItem","Style", "Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate", "CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", "FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference", "XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -125,6 +125,7 @@ $childTypeDirMap = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles" "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules" "CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
"Bot"="Bots"
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages" "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences" "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-validate v1.3 — Validate 1C configuration XML structure # cf-validate v1.4 — Validate 1C configuration XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages.""" """Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
import sys, os, argparse, re import sys, os, argparse, re
@@ -33,11 +33,11 @@ VALID_CLASS_IDS = [
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface 'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
] ]
# 44 types in canonical order # 45 types in canonical order
CHILD_OBJECT_TYPES = [ CHILD_OBJECT_TYPES = [
'Language', 'Subsystem', 'StyleItem', 'Style', 'Language', 'Subsystem', 'StyleItem', 'Style',
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate', 'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan', 'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference', 'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption', 'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup', 'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
@@ -54,6 +54,7 @@ CHILD_TYPE_DIR_MAP = {
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles', 'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles', 'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules', 'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
'Bot': 'Bots',
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages', 'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences', 'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs', 'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
+206 -112
View File
@@ -1,4 +1,4 @@
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE) # cfe-borrow v1.9 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)][string]$ExtensionPath, [Parameter(Mandatory)][string]$ExtensionPath,
@@ -13,6 +13,31 @@ $ErrorActionPreference = "Stop"
function Info([string]$msg) { Write-Host "[INFO] $msg" } function Info([string]$msg) { Write-Host "[INFO] $msg" }
function Warn([string]$msg) { Write-Host "[WARN] $msg" } function Warn([string]$msg) { Write-Host "[WARN] $msg" }
# Form data-binding tags (value = attribute path). A binding survives only if its root
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
# platform rejects the form with "Неверный путь к данным" on load.
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath')
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
$script:formBindingPictureTags = @('RowPictureDataPath','MultipleValuePictureDataPath')
# Strip data-binding tags whose root attribute isn't borrowed.
# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped.
function Strip-FormBindings {
param([string]$xml, [bool]$keepObjekt)
foreach ($tag in $script:formBindingDataTags) {
if ($keepObjekt) {
$xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*</$tag>", '')
} else {
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
}
}
foreach ($tag in $script:formBindingPictureTags) {
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
}
return $xml
}
# --- 1. Resolve paths --- # --- 1. Resolve paths ---
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
@@ -419,6 +444,14 @@ function Read-SourceObject {
$srcProps[$propName] = $propNode.InnerText.Trim() $srcProps[$propName] = $propNode.InnerText.Trim()
} }
} }
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
if ($typeName -eq "DefinedType") {
$typeNode = $propsNode.SelectSingleNode("md:Type", $srcNs)
if ($typeNode) {
$srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
}
}
} }
return @{ return @{
@@ -481,8 +514,23 @@ function Borrow-Form {
} }
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc) $srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
# 3. Generate form metadata XML (ФормаЭлемента.xml) # 3. Generate form metadata XML (ФормаЭлемента.xml).
$newFormUuid = [guid]::NewGuid().ToString() # If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
# (regenerating it would churn the form's identity on every rerun).
$formMetaFileExisting = Join-Path (Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms") "${formName}.xml"
$newFormUuid = ""
if (Test-Path $formMetaFileExisting) {
try {
$existingDoc = New-Object System.Xml.XmlDocument
$existingDoc.Load($formMetaFileExisting)
$existingFormNode = $existingDoc.DocumentElement.SelectSingleNode("*[local-name()='Form']")
if ($existingFormNode) {
$existingUuid = $existingFormNode.GetAttribute("uuid")
if ($existingUuid) { $newFormUuid = $existingUuid }
}
} catch { }
}
if (-not $newFormUuid) { $newFormUuid = [guid]::NewGuid().ToString() }
$formMetaSb = New-Object System.Text.StringBuilder $formMetaSb = New-Object System.Text.StringBuilder
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null $formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null $formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
@@ -516,8 +564,10 @@ function Borrow-Form {
$srcFormDoc.Load($srcFormXmlPath) $srcFormDoc.Load($srcFormXmlPath)
$srcFormEl = $srcFormDoc.DocumentElement $srcFormEl = $srcFormDoc.DocumentElement
$formVersion = $srcFormEl.GetAttribute("version") # Borrowed form must use the extension's format version (not the source form's), so the whole
if (-not $formVersion) { $formVersion = $script:formatVersion } # extension stays uniform — otherwise the platform rejects the import on a version mismatch
# (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version.
$formVersion = $script:formatVersion
# Find direct children: form properties, AutoCommandBar, ChildItems # Find direct children: form properties, AutoCommandBar, ChildItems
$srcAutoCmd = $null $srcAutoCmd = $null
@@ -552,13 +602,8 @@ function Borrow-Form {
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>' $autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
# Strip ExcludedCommand (references to standard commands invalid in extension) # Strip ExcludedCommand (references to standard commands invalid in extension)
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '') $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
# Strip DataPath in AutoCommandBar buttons # Strip data-binding tags whose root attribute isn't borrowed
if ($BorrowMainAttr) { $autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr)
# Keep only Объект.* DataPaths
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
} else {
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>[^<]*</DataPath>', '')
}
} }
# ChildItems: copy full tree, clean up base-config references # ChildItems: copy full tree, clean up base-config references
@@ -568,17 +613,9 @@ function Borrow-Form {
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '') $childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
# Replace all CommandName values with 0 # Replace all CommandName values with 0
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>') $childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
# Strip DataPath, TitleDataPath, RowPictureDataPath # Strip data-binding tags whose root attribute isn't borrowed
if ($BorrowMainAttr) { # (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed) $childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr)
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>(?!Объект\.)[^<]*</TitleDataPath>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
} else {
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>[^<]*</DataPath>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>[^<]*</TitleDataPath>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
}
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) # Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '') $childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID) # Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
@@ -855,14 +892,19 @@ function Borrow-Form {
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc) [System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
Info " Created: $formXmlFile" Info " Created: $formXmlFile"
# 6. Create empty Module.bsl # 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
# not clobber user code added to the form module).
$moduleDir = Join-Path $formXmlDir "Form" $moduleDir = Join-Path $formXmlDir "Form"
if (-not (Test-Path $moduleDir)) { if (-not (Test-Path $moduleDir)) {
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
} }
$moduleBslFile = Join-Path $moduleDir "Module.bsl" $moduleBslFile = Join-Path $moduleDir "Module.bsl"
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc) if (Test-Path $moduleBslFile) {
Info " Created: $moduleBslFile" Info " Preserved existing Module.bsl"
} else {
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
Info " Created: $moduleBslFile"
}
# 7. Register form in parent object ChildObjects # 7. Register form in parent object ChildObjects
Register-FormInObject $typeName $objName $formName Register-FormInObject $typeName $objName $formName
@@ -1010,8 +1052,29 @@ function Collect-FormDataPaths {
$firstLevel = @{} $firstLevel = @{}
$deepPaths = @() $deepPaths = @()
$matches2 = [regex]::Matches($content, '<DataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</DataPath>') # Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
foreach ($m in $matches2) { # for Объект.* references — picture-path tags carry picture indices, not data attributes.
foreach ($tag in $script:formBindingDataTags) {
$bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</$tag>")
foreach ($m in $bms) {
$path = $m.Groups[1].Value
$segments = $path.Split(".")
$seg0 = $segments[0]
if ($script:standardFields -contains $seg0) { continue }
$firstLevel[$seg0] = $true
if ($segments.Count -ge 2) {
$seg1 = $segments[1]
if ($script:standardFields -contains $seg1) { continue }
$seg2 = if ($segments.Count -ge 3) { $segments[2] } else { $null }
$deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1; SubSubAttr = $seg2 }
}
}
}
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
$fieldMatches = [regex]::Matches($content, "<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>")
foreach ($m in $fieldMatches) {
$path = $m.Groups[1].Value $path = $m.Groups[1].Value
$segments = $path.Split(".") $segments = $path.Split(".")
$seg0 = $segments[0] $seg0 = $segments[0]
@@ -1024,21 +1087,11 @@ function Collect-FormDataPaths {
} }
} }
# Also collect from TitleDataPath
$matches3 = [regex]::Matches($content, '<TitleDataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</TitleDataPath>')
foreach ($m in $matches3) {
$path = $m.Groups[1].Value
$segments = $path.Split(".")
$seg0 = $segments[0]
if ($script:standardFields -contains $seg0) { continue }
$firstLevel[$seg0] = $true
}
# Deduplicate deep paths # Deduplicate deep paths
$seen = @{} $seen = @{}
$uniqueDeep = @() $uniqueDeep = @()
foreach ($dp in $deepPaths) { foreach ($dp in $deepPaths) {
$key = "$($dp.ObjectAttr).$($dp.SubAttr)" $key = "$($dp.ObjectAttr).$($dp.SubAttr).$($dp.SubSubAttr)"
if (-not $seen.ContainsKey($key)) { if (-not $seen.ContainsKey($key)) {
$seen[$key] = $true $seen[$key] = $true
$uniqueDeep += $dp $uniqueDeep += $dp
@@ -1142,7 +1195,8 @@ function Resolve-SourceAttributes {
} }
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.) # Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
$extraProps = @{} # Ordered so PS emits the same property order as the Python port (dict preserves insertion order).
$extraProps = [ordered]@{}
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs) $propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
if ($propsNode) { if ($propsNode) {
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength", $propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
@@ -1375,28 +1429,45 @@ function Borrow-MainAttribute {
# Step 3: Build the adopted content and insert into main object XML # Step 3: Build the adopted content and insert into main object XML
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
# Read existing object XML (needed for dedup + enrichment)
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
$existingChildNames = @{}
if ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
foreach ($nm in [regex]::Matches($Matches[1], '<Name>(\w+)</Name>')) {
$existingChildNames[$nm.Groups[1].Value] = $true
}
}
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
$insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
# Generate full object XML with attributes and TS # Generate full object XML with attributes and TS
$contentSb = New-Object System.Text.StringBuilder $contentSb = New-Object System.Text.StringBuilder
foreach ($attr in $srcAttrs) { foreach ($attr in $insertAttrs) {
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" $attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
$contentSb.AppendLine($attrXml) | Out-Null $contentSb.AppendLine($attrXml) | Out-Null
} }
foreach ($ts in $srcTS) { foreach ($ts in $insertTS) {
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t" $tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
$contentSb.AppendLine($tsXml) | Out-Null $contentSb.AppendLine($tsXml) | Out-Null
} }
$adoptedContent = $contentSb.ToString().TrimEnd() $adoptedContent = $contentSb.ToString().TrimEnd()
# Read existing object XML and inject # Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true))) # first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
# Inject extra properties after ExtendedConfigurationObject
if ($extraProps.Count -gt 0) { if ($extraProps.Count -gt 0) {
$objPropsBlock = ""
if ($objContent -match '(?s)<Properties>(.*?)</Properties>') { $objPropsBlock = $Matches[1] }
$propsSb = New-Object System.Text.StringBuilder $propsSb = New-Object System.Text.StringBuilder
foreach ($pName in $extraProps.Keys) { foreach ($pName in $extraProps.Keys) {
if ($objPropsBlock -match "<$pName>") { continue }
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null $propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
} }
$objContent = $objContent -replace '(</ExtendedConfigurationObject>)', "`$1$($propsSb.ToString())" if ($propsSb.Length -gt 0) {
$objContent = ([regex]'</ExtendedConfigurationObject>').Replace($objContent, "</ExtendedConfigurationObject>$($propsSb.ToString())", 1)
}
} }
# Replace empty ChildObjects with adopted content # Replace empty ChildObjects with adopted content
@@ -1454,79 +1525,46 @@ function Borrow-MainAttribute {
# Step 5: Handle deep paths (Form mode only) # Step 5: Handle deep paths (Form mode only)
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) { if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
# Filter out deep paths where ObjectAttr is a TabularSection (those are TS column refs, not deep attribute refs) # Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
$realDeep = @() $deepByAttr = @{}
foreach ($dp in $deepPaths) { foreach ($dp in $deepPaths) {
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { $realDeep += $dp } if ($tsNames.ContainsKey($dp.ObjectAttr)) { continue }
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
if ($deepByAttr[$dp.ObjectAttr] -notcontains $dp.SubAttr) { $deepByAttr[$dp.ObjectAttr] += $dp.SubAttr }
} }
if ($deepByAttr.Count -gt 0) {
if ($realDeep.Count -gt 0) { Info " Processing $($deepByAttr.Count) deep path attribute(s)..."
Info " Processing $($realDeep.Count) deep path(s)..."
# Group by ObjectAttr → target catalog
$deepByAttr = @{}
foreach ($dp in $realDeep) {
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
$deepByAttr[$dp.ObjectAttr] += $dp.SubAttr
}
foreach ($attrName in $deepByAttr.Keys) { foreach ($attrName in $deepByAttr.Keys) {
# Find the attribute's type to determine target catalog
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1 $attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
if (-not $attrInfo) { continue } if (-not $attrInfo) { continue }
# Extract catalog name from type: cfg:CatalogRef.XXX
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)') $catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
if (-not $catMatch.Success) { continue } if (-not $catMatch.Success) { continue }
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $deepByAttr[$attrName]
}
}
$targetTypeName = $catMatch.Groups[1].Value # Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
$targetObjName = $catMatch.Groups[2].Value $tsDeepByCol = @{}
foreach ($dp in $deepPaths) {
# Ensure target is borrowed if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { continue }
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) { if (-not $dp.SubSubAttr) { continue }
$tSrc = Read-SourceObject $targetTypeName $targetObjName if ($script:standardFields -contains $dp.SubSubAttr) { continue }
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties $k = "$($dp.ObjectAttr)|$($dp.SubAttr)"
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName] if (-not $tsDeepByCol.ContainsKey($k)) { $tsDeepByCol[$k] = @() }
if (-not (Test-Path $tTargetDir)) { if ($tsDeepByCol[$k] -notcontains $dp.SubSubAttr) { $tsDeepByCol[$k] += $dp.SubSubAttr }
New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null }
} if ($tsDeepByCol.Count -gt 0) {
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml" Info " Processing $($tsDeepByCol.Count) tabular-section deep path(s)..."
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBom) foreach ($k in $tsDeepByCol.Keys) {
Add-ToChildObjects $targetTypeName $targetObjName $parts = $k.Split("|")
$script:borrowedFiles += $tTargetFile $tsName = $parts[0]; $colName = $parts[1]
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}" $tsInfo = $srcTS | Where-Object { $_.Name -eq $tsName } | Select-Object -First 1
} if (-not $tsInfo) { continue }
$colInfo = $tsInfo.Attributes | Where-Object { $_.Name -eq $colName } | Select-Object -First 1
# Resolve sub-attributes in target catalog if (-not $colInfo) { continue }
$subNames = @{} $catMatch = [regex]::Match($colInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
foreach ($sn in $deepByAttr[$attrName]) { $subNames[$sn] = $true } if (-not $catMatch.Success) { continue }
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $tsDeepByCol[$k]
if ($subResolved.Attributes.Count -gt 0) {
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
# Collect and borrow ref types from deep attributes
$subTypeXmls = @()
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
foreach ($srt in $subRefTypes) {
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
if (-not (Test-Path $sSrcFile)) { continue }
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
if (-not (Test-Path $sTargetDir)) {
New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null
}
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBom)
Add-ToChildObjects $srt.TypeName $srt.ObjName
$script:borrowedFiles += $sTargetFile
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
}
}
} }
} }
} }
@@ -1534,6 +1572,57 @@ function Borrow-MainAttribute {
Info " Main attribute borrowing complete" Info " Main attribute borrowing complete"
} }
# --- 11i. Helper: borrow a deep-path target catalog together with the referenced sub-attributes ---
# Used for both Объект.<Ref>.<Sub> (top-level ref attr) and Объект.<ТЧ>.<Колонка>.<Sub> (tabular-section
# column ref). Mirrors Designer: the referenced catalog is adopted WITH the sub-attributes the form shows,
# otherwise the platform rejects the deep DataPath ("Неверный путь к данным").
function Borrow-DeepTargetAttrs {
param([string]$targetTypeName, [string]$targetObjName, $subAttrNames)
$encBomLocal = New-Object System.Text.UTF8Encoding($true)
# Ensure target is borrowed (shell)
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
$tSrc = Read-SourceObject $targetTypeName $targetObjName
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
if (-not (Test-Path $tTargetDir)) { New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null }
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBomLocal)
Add-ToChildObjects $targetTypeName $targetObjName
$script:borrowedFiles += $tTargetFile
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
}
# Resolve sub-attributes in target catalog and merge them in
$subNames = @{}
foreach ($sn in $subAttrNames) { $subNames[$sn] = $true }
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
if ($subResolved.Attributes.Count -gt 0) {
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
# Borrow ref types referenced by the sub-attributes
$subTypeXmls = @()
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
foreach ($srt in $subRefTypes) {
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
if (-not (Test-Path $sSrcFile)) { continue }
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
if (-not (Test-Path $sTargetDir)) { New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null }
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBomLocal)
Add-ToChildObjects $srt.TypeName $srt.ObjName
$script:borrowedFiles += $sTargetFile
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
}
}
}
# --- 12. Helper: build borrowed object XML --- # --- 12. Helper: build borrowed object XML ---
function Build-BorrowedObjectXml { function Build-BorrowedObjectXml {
param( param(
@@ -1572,6 +1661,11 @@ function Build-BorrowedObjectXml {
} }
} }
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
if ($typeName -eq "DefinedType" -and $sourceProps.ContainsKey("__TypeXml")) {
$sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null
}
$sb.AppendLine("`t`t</Properties>") | Out-Null $sb.AppendLine("`t`t</Properties>") | Out-Null
# ChildObjects (for types that need it) # ChildObjects (for types that need it)
+243 -118
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE) # cfe-borrow v1.9 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,36 @@ XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
V8_NS = "http://v8.1c.ru/8.1/data/core" V8_NS = "http://v8.1c.ru/8.1/data/core"
# Form data-binding tags (value = attribute path). A binding survives only if its root
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
# platform rejects the form with "Неверный путь к данным" on load.
FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath", "MultipleValueDataPath", "MultipleValuePresentDataPath"]
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
FORM_BINDING_PICTURE_TAGS = ["RowPictureDataPath", "MultipleValuePictureDataPath"]
def strip_form_bindings(xml, keep_objekt):
"""Strip data-binding tags whose root attribute isn't borrowed.
keep_objekt=True (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
keep_objekt=False (default skeleton): strip all bindings. Picture-path tags are always stripped."""
for tag in FORM_BINDING_DATA_TAGS:
if keep_objekt:
xml = re.sub(rf'\s*<{tag}>(?!Объект\.)[^<]*</{tag}>', '', xml)
else:
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
for tag in FORM_BINDING_PICTURE_TAGS:
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
return xml
def decode_numeric_entities(s):
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
elements where the PowerShell port writes literal characters. Normalize numeric refs
back to literal so PS↔PY output matches. Named entities (&amp; &lt; ...) are left intact."""
s = re.sub(r'&#x([0-9A-Fa-f]+);', lambda m: chr(int(m.group(1), 16)), s)
s = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), s)
return s
def localname(el): def localname(el):
return etree.QName(el.tag).localname return etree.QName(el.tag).localname
@@ -319,13 +349,49 @@ def expand_self_closing(container, parent_indent):
container.text = "\r\n" + parent_indent container.text = "\r\n" + parent_indent
def save_xml_bom(tree, path): def _detect_xml_style(path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>') финальный перенос. None → файл новый (сохранить текущее поведение)."""
if not xml_bytes.endswith(b"\n"): try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_bom(tree, path):
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf") if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes) f.write(xml_bytes)
@@ -462,6 +528,13 @@ def main():
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}") prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
if prop_node is not None: if prop_node is not None:
src_props[prop_name] = (prop_node.text or "").strip() src_props[prop_name] = (prop_node.text or "").strip()
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
if type_name == "DefinedType":
type_node = props_node.find(f"{{{MD_NS}}}Type")
if type_node is not None:
type_xml = etree.tostring(type_node, encoding="unicode")
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el} return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
@@ -533,6 +606,10 @@ def main():
prop_val = source_props.get(prop_name, "false") prop_val = source_props.get(prop_name, "false")
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>") lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
if type_name == "DefinedType" and "__TypeXml" in source_props:
lines.append(f"\t\t\t{source_props['__TypeXml']}")
lines.append("\t\t</Properties>") lines.append("\t\t</Properties>")
if type_name in TYPES_WITH_CHILD_OBJECTS: if type_name in TYPES_WITH_CHILD_OBJECTS:
@@ -644,7 +721,26 @@ def main():
first_level = {} first_level = {}
deep_paths = [] deep_paths = []
for m in re.finditer(r'<DataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</DataPath>', content): # Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
for tag in FORM_BINDING_DATA_TAGS:
for m in re.finditer(r'<' + tag + r'>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</' + tag + r'>', content):
path = m.group(1)
segments = path.split(".")
seg0 = segments[0]
if seg0 in STANDARD_FIELDS:
continue
first_level[seg0] = True
if len(segments) >= 2:
seg1 = segments[1]
if seg1 in STANDARD_FIELDS:
continue
seg2 = segments[2] if len(segments) >= 3 else None
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
for m in re.finditer(r'<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>', content):
path = m.group(1) path = m.group(1)
segments = path.split(".") segments = path.split(".")
seg0 = segments[0] seg0 = segments[0]
@@ -655,22 +751,14 @@ def main():
seg1 = segments[1] seg1 = segments[1]
if seg1 in STANDARD_FIELDS: if seg1 in STANDARD_FIELDS:
continue continue
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1}) seg2 = segments[2] if len(segments) >= 3 else None
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
# Also collect from TitleDataPath
for m in re.finditer(r'<TitleDataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</TitleDataPath>', content):
path = m.group(1)
segments = path.split(".")
seg0 = segments[0]
if seg0 in STANDARD_FIELDS:
continue
first_level[seg0] = True
# Deduplicate deep paths # Deduplicate deep paths
seen = set() seen = set()
unique_deep = [] unique_deep = []
for dp in deep_paths: for dp in deep_paths:
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}" key = f"{dp['ObjectAttr']}.{dp['SubAttr']}.{dp.get('SubSubAttr')}"
if key not in seen: if key not in seen:
seen.add(key) seen.add(key)
unique_deep.append(dp) unique_deep.append(dp)
@@ -941,26 +1029,40 @@ def main():
# Step 3: Build the adopted content and insert into main object XML # Step 3: Build the adopted content and insert into main object XML
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml") obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
# Generate full object XML with attributes and TS # Read existing object XML (needed for dedup + enrichment)
content_parts = []
for attr in src_attrs:
attr_xml = build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t")
content_parts.append(attr_xml)
for ts in src_ts:
ts_xml = build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t")
content_parts.append(ts_xml)
adopted_content = "\n".join(content_parts).rstrip()
# Read existing object XML and inject
with open(obj_file, "r", encoding="utf-8-sig") as fh: with open(obj_file, "r", encoding="utf-8-sig") as fh:
obj_content = fh.read() obj_content = fh.read()
# Inject extra properties after ExtendedConfigurationObject # Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
existing_child_names = set()
m_co = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
if m_co:
for nm in re.findall(r'<Name>(\w+)</Name>', m_co.group(1)):
existing_child_names.add(nm)
insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names]
insert_ts = [t for t in src_ts if t["Name"] not in existing_child_names]
# Generate full object XML with attributes and TS
content_parts = []
for attr in insert_attrs:
content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t"))
for ts in insert_ts:
content_parts.append(build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t"))
adopted_content = "\n".join(content_parts).rstrip()
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
if extra_props: if extra_props:
m_props = re.search(r'(?s)<Properties>(.*?)</Properties>', obj_content)
obj_props_block = m_props.group(1) if m_props else ""
props_xml = "" props_xml = ""
for p_name, p_val in extra_props.items(): for p_name, p_val in extra_props.items():
if f"<{p_name}>" in obj_props_block:
continue
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>" props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}") if props_xml:
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
# Replace empty ChildObjects with adopted content # Replace empty ChildObjects with adopted content
if adopted_content: if adopted_content:
@@ -1012,79 +1114,93 @@ def main():
# Step 5: Handle deep paths (Form mode only) # Step 5: Handle deep paths (Form mode only)
if mode == "Form" and deep_paths: if mode == "Form" and deep_paths:
# Filter out deep paths where ObjectAttr is a TabularSection # Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names] deep_by_attr = {}
for dp in deep_paths:
if real_deep: if dp["ObjectAttr"] in ts_names:
info(f" Processing {len(real_deep)} deep path(s)...") continue
deep_by_attr.setdefault(dp["ObjectAttr"], [])
# Group by ObjectAttr -> target catalog if dp["SubAttr"] not in deep_by_attr[dp["ObjectAttr"]]:
deep_by_attr = {}
for dp in real_deep:
if dp["ObjectAttr"] not in deep_by_attr:
deep_by_attr[dp["ObjectAttr"]] = []
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"]) deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
if deep_by_attr:
info(f" Processing {len(deep_by_attr)} deep path attribute(s)...")
for attr_name, sub_attr_names in deep_by_attr.items(): for attr_name, sub_attr_names in deep_by_attr.items():
# Find the attribute's type to determine target catalog attr_info = next((a for a in src_attrs if a["Name"] == attr_name), None)
attr_info = None
for a in src_attrs:
if a["Name"] == attr_name:
attr_info = a
break
if not attr_info: if not attr_info:
continue continue
# Extract catalog name from type: cfg:CatalogRef.XXX
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"]) cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
if not cat_match: if not cat_match:
continue continue
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
target_type_name = cat_match.group(1) # Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
target_obj_name = cat_match.group(2) ts_deep_by_col = {}
for dp in deep_paths:
# Ensure target is borrowed if dp["ObjectAttr"] not in ts_names:
if not test_object_borrowed(target_type_name, target_obj_name): continue
t_src = read_source_object(target_type_name, target_obj_name) if not dp.get("SubSubAttr"):
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"]) continue
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name]) if dp["SubSubAttr"] in STANDARD_FIELDS:
os.makedirs(t_target_dir, exist_ok=True) continue
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml") k = (dp["ObjectAttr"], dp["SubAttr"])
save_text_bom(t_target_file, t_borrowed_xml) ts_deep_by_col.setdefault(k, [])
add_to_child_objects(target_type_name, target_obj_name) if dp["SubSubAttr"] not in ts_deep_by_col[k]:
borrowed_files.append(t_target_file) ts_deep_by_col[k].append(dp["SubSubAttr"])
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}") if ts_deep_by_col:
info(f" Processing {len(ts_deep_by_col)} tabular-section deep path(s)...")
# Resolve sub-attributes in target catalog for (ts_name, col_name), sub_attr_names in ts_deep_by_col.items():
sub_names = {sn: True for sn in sub_attr_names} ts_info = next((t for t in src_ts if t["Name"] == ts_name), None)
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names) if not ts_info:
continue
if sub_resolved["Attributes"]: col_info = next((c for c in ts_info["Attributes"] if c["Name"] == col_name), None)
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"]) if not col_info:
continue
# Collect and borrow ref types from deep attributes cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', col_info["TypeXml"])
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]] if not cat_match:
sub_ref_types = collect_reference_types(sub_type_xmls) continue
for srt in sub_ref_types: borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
continue
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
continue
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
if not os.path.isfile(s_src_file):
continue
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
os.makedirs(s_target_dir, exist_ok=True)
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
save_text_bom(s_target_file, s_borrowed_xml)
add_to_child_objects(srt["TypeName"], srt["ObjName"])
borrowed_files.append(s_target_file)
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
info(" Main attribute borrowing complete") info(" Main attribute borrowing complete")
def borrow_deep_target_attrs(target_type_name, target_obj_name, sub_attr_names):
# Borrow a deep-path target catalog together with the referenced sub-attributes, for both
# Объект.<Ref>.<Sub> and Объект.<ТЧ>.<Колонка>.<Sub>. Mirrors Designer: the referenced catalog
# is adopted WITH the sub-attributes the form shows, else the platform rejects the deep DataPath.
if not test_object_borrowed(target_type_name, target_obj_name):
t_src = read_source_object(target_type_name, target_obj_name)
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
os.makedirs(t_target_dir, exist_ok=True)
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
save_text_bom(t_target_file, t_borrowed_xml)
add_to_child_objects(target_type_name, target_obj_name)
borrowed_files.append(t_target_file)
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
sub_names = {sn: True for sn in sub_attr_names}
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
if sub_resolved["Attributes"]:
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
sub_ref_types = collect_reference_types(sub_type_xmls)
for srt in sub_ref_types:
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
continue
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
continue
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
if not os.path.isfile(s_src_file):
continue
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
os.makedirs(s_target_dir, exist_ok=True)
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
save_text_bom(s_target_file, s_borrowed_xml)
add_to_child_objects(srt["TypeName"], srt["ObjName"])
borrowed_files.append(s_target_file)
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False): def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
dir_name = CHILD_TYPE_DIR_MAP[type_name] dir_name = CHILD_TYPE_DIR_MAP[type_name]
@@ -1100,8 +1216,22 @@ def main():
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh: with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
src_form_content = fh.read() src_form_content = fh.read()
# 3. Generate form metadata XML # 3. Generate form metadata XML.
new_form_uuid = new_guid() # If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
# (regenerating it would churn the form's identity on every rerun).
existing_wrapper = os.path.join(ext_dir, dir_name, obj_name, "Forms", f"{form_name}.xml")
new_form_uuid = ""
if os.path.isfile(existing_wrapper):
try:
existing_root = etree.parse(existing_wrapper).getroot()
for c in existing_root:
if isinstance(c.tag, str) and localname(c) == "Form":
new_form_uuid = c.get("uuid", "") or ""
break
except Exception:
new_form_uuid = ""
if not new_form_uuid:
new_form_uuid = new_guid()
form_meta_lines = [ form_meta_lines = [
'<?xml version="1.0" encoding="UTF-8"?>', '<?xml version="1.0" encoding="UTF-8"?>',
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">', f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
@@ -1131,7 +1261,10 @@ def main():
src_form_tree = etree.parse(src_form_xml_path, src_form_parser) src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
src_form_el = src_form_tree.getroot() src_form_el = src_form_tree.getroot()
form_version = src_form_el.get("version", format_version) # Borrowed form uses the extension's format version (not the source form's) — keeps the
# extension uniform; otherwise the platform rejects the import on a version mismatch
# (e.g. a 2.13 form inside a 2.17 extension). The platform upgrades the form to the root version.
form_version = format_version
src_auto_cmd = None src_auto_cmd = None
form_props = [] form_props = []
@@ -1149,25 +1282,21 @@ def main():
continue continue
if not reached_visual: if not reached_visual:
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.) # Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
form_props.append(etree.tostring(fc, encoding="unicode")) form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode")))
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false # AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
auto_cmd_xml = "" auto_cmd_xml = ""
if src_auto_cmd is not None: if src_auto_cmd is not None:
auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode") auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode"))
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml) auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml) auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>') auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
# Strip ExcludedCommand (references to standard commands invalid in extension) # Strip ExcludedCommand (references to standard commands invalid in extension)
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml) auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
# Strip DataPath in AutoCommandBar buttons # Strip data-binding tags whose root attribute isn't borrowed
if borrow_main_attr: auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
# Keep only Объект.* DataPaths
auto_cmd_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', auto_cmd_xml)
else:
auto_cmd_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', auto_cmd_xml)
# ChildItems: copy full tree, clean up base-config references # ChildItems: copy full tree, clean up base-config references
child_items_xml = "" child_items_xml = ""
@@ -1178,20 +1307,12 @@ def main():
break break
if src_child_items is not None: if src_child_items is not None:
child_items_xml = etree.tostring(src_child_items, encoding="unicode") child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode"))
child_items_xml = ns_strip_pattern.sub("", child_items_xml) child_items_xml = ns_strip_pattern.sub("", child_items_xml)
# Replace all CommandName values with 0 # Replace all CommandName values with 0
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml) child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
# Strip DataPath / TitleDataPath / RowPictureDataPath # Strip data-binding tags whose root attribute isn't borrowed
if borrow_main_attr: child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
child_items_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<TitleDataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</TitleDataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
else:
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) # Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml) child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
# Strip TypeLink blocks with human-readable DataPath (Items.XXX) # Strip TypeLink blocks with human-readable DataPath (Items.XXX)
@@ -1428,12 +1549,16 @@ def main():
save_text_bom(form_xml_file, "".join(parts)) save_text_bom(form_xml_file, "".join(parts))
info(f" Created: {form_xml_file}") info(f" Created: {form_xml_file}")
# 6. Create empty Module.bsl # 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
# not clobber user code added to the form module).
module_dir = os.path.join(form_xml_dir, "Form") module_dir = os.path.join(form_xml_dir, "Form")
os.makedirs(module_dir, exist_ok=True) os.makedirs(module_dir, exist_ok=True)
module_bsl_file = os.path.join(module_dir, "Module.bsl") module_bsl_file = os.path.join(module_dir, "Module.bsl")
save_text_bom(module_bsl_file, "") if os.path.isfile(module_bsl_file):
info(f" Created: {module_bsl_file}") info(" Preserved existing Module.bsl")
else:
save_text_bom(module_bsl_file, "")
info(f" Created: {module_bsl_file}")
# 7. Register form in parent object ChildObjects # 7. Register form in parent object ChildObjects
register_form_in_object(type_name, obj_name, form_name) register_form_in_object(type_name, obj_name, form_name)
+13 -4
View File
@@ -1,4 +1,4 @@
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE) # cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -35,6 +35,10 @@ if (Test-Path $cfgFile) {
exit 1 exit 1
} }
# MDClasses format version — inherited from the base config so the extension stays uniform
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
$formatVersion = "2.17"
# --- Resolve ConfigPath --- # --- Resolve ConfigPath ---
$baseLangUuid = "00000000-0000-0000-0000-000000000000" $baseLangUuid = "00000000-0000-0000-0000-000000000000"
if ($ConfigPath) { if ($ConfigPath) {
@@ -75,6 +79,11 @@ if ($ConfigPath) {
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path) $baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable) $baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") $baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version")
if ($fmtVer) {
$formatVersion = $fmtVer
Write-Host "[INFO] Base config format version: $formatVersion"
}
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs) $compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
if ($compatNode -and $compatNode.InnerText) { if ($compatNode -and $compatNode.InnerText) {
$CompatibilityMode = $compatNode.InnerText.Trim() $CompatibilityMode = $compatNode.InnerText.Trim()
@@ -138,7 +147,7 @@ $childObjectsXml += "`r`n`t`t"
# --- Configuration.xml --- # --- Configuration.xml ---
$cfgXml = @" $cfgXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
<Configuration uuid="$uuidCfg"> <Configuration uuid="$uuidCfg">
<InternalInfo> <InternalInfo>
<xr:ContainedObject> <xr:ContainedObject>
@@ -203,7 +212,7 @@ $cfgXml = @"
# --- Languages/Русский.xml (adopted format) --- # --- Languages/Русский.xml (adopted format) ---
$langXml = @" $langXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
<Language uuid="$uuidLang"> <Language uuid="$uuidLang">
<InternalInfo/> <InternalInfo/>
<Properties> <Properties>
@@ -220,7 +229,7 @@ $langXml = @"
# --- Role XML --- # --- Role XML ---
$roleXml = @" $roleXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
<Role uuid="$uuidRole"> <Role uuid="$uuidRole">
<Properties> <Properties>
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name> <Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
+12 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE) # cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration extension.""" """Generates minimal XML source files for a 1C configuration extension."""
import sys, os, argparse, uuid import sys, os, argparse, uuid
@@ -50,6 +50,10 @@ def main():
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr) print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# MDClasses format version — inherited from the base config so the extension stays uniform
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
format_version = "2.17"
# --- Resolve ConfigPath --- # --- Resolve ConfigPath ---
base_lang_uuid = "00000000-0000-0000-0000-000000000000" base_lang_uuid = "00000000-0000-0000-0000-000000000000"
if args.ConfigPath: if args.ConfigPath:
@@ -88,6 +92,10 @@ def main():
try: try:
base_cfg_tree = ET.parse(os.path.abspath(config_path)) base_cfg_tree = ET.parse(os.path.abspath(config_path))
base_cfg_root = base_cfg_tree.getroot() base_cfg_root = base_cfg_tree.getroot()
fmt_ver = base_cfg_root.get("version")
if fmt_ver:
format_version = fmt_ver
print(f"[INFO] Base config format version: {format_version}")
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'} ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns) compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
if compat_node is not None and compat_node.text: if compat_node is not None and compat_node.text:
@@ -155,7 +163,7 @@ def main():
\t\t\t</xr:ContainedObject>\n""" \t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?> cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
\t<Configuration uuid="{uuid_cfg}"> \t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo> \t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo> {contained_objects}\t\t</InternalInfo>
@@ -190,7 +198,7 @@ def main():
# --- Languages/Русский.xml (adopted format) --- # --- Languages/Русский.xml (adopted format) ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?> lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
\t<Language uuid="{uuid_lang}"> \t<Language uuid="{uuid_lang}">
\t\t<InternalInfo/> \t\t<InternalInfo/>
\t\t<Properties> \t\t<Properties>
@@ -205,7 +213,7 @@ def main():
# --- Role XML --- # --- Role XML ---
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?> role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
\t<Role uuid="{uuid_role}"> \t<Role uuid="{uuid_role}">
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>{esc_xml(role_name)}</Name> \t\t\t<Name>{esc_xml(role_name)}</Name>
+95 -28
View File
@@ -1,7 +1,7 @@
--- ---
name: cfe-patch-method name: cfe-patch-method
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального description: Генерация и актуализация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после, вместо оригинала, изменить его тело (ИзменениеИКонтроль) — или актуализировать перехватчик после изменения оригинала
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before argument-hint: -ExtensionPath <path> -ConfigPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
@@ -10,22 +10,31 @@ allowed-tools:
# /cfe-patch-method — Генерация перехватчика метода # /cfe-patch-method — Генерация перехватчика метода
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий. Читает оригинал метода из конфигурации-источника и генерирует `.bsl`-перехватчик заимствованного объекта: с правильной директивой контекста, полной сигнатурой, обрамляющими инструкциями препроцессора и областями. Для `ИзменениеИКонтроль` копирует всё тело оригинала. Создаёт файл модуля, дописывает в существующий или актуализирует уже заимствованный метод.
## Предусловие ## Предусловие
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры. Объект должен быть заимствован в расширение (`/cfe-borrow`). Нужен доступ к исходникам базовой конфигурации (`-ConfigPath`) — оттуда читается оригинал метода. `NamePrefix` берётся из `Configuration.xml` расширения.
### Авто-определение ConfigPath
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя
## Параметры ## Параметры
| Параметр | Описание | По умолчанию | | Параметр | Описание | По умолчанию |
|----------|----------|--------------| |----------|----------|--------------|
| `ExtensionPath` | Путь к расширению (обязат.) | — | | `ExtensionPath` | Путь к расширению (обязат.) | — |
| `ModulePath` | Путь к модулю (обязат.) | — | | `ConfigPath` | Путь к конфигурации-источнику | обязат., кроме случая, когда `ModulePath` — путь к файлу |
| `MethodName` | Имя перехватываемого метода (обязат.) | — | | `ModulePath` | Логическое имя (`Тип.Имя.Модуль`) **или** путь к файлу модуля `.bsl` | обязат. для генерации |
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — | | `MethodName` | Имя перехватываемого метода | обязат. для генерации |
| `Context` | Директива контекста | `НаСервере` | | `InterceptorType` | `Before` / `After` / `Instead` / `ModificationAndControl` | обязат. для генерации |
| `IsFunction` | Метод — функция (добавит `Возврат`) | false | | `Check` | Проверить контролируемые методы на дрейф (только отчёт) | — |
| `Actualize` | Актуализировать дрейфнувшие контролируемые методы | — |
## Формат ModulePath ## Формат ModulePath
@@ -40,39 +49,97 @@ allowed-tools:
Аналогично для Report, DataProcessor, InformationRegister и других типов. Аналогично для Report, DataProcessor, InformationRegister и других типов.
Вместо логического имени в `ModulePath` можно передать **путь к файлу** модуля-источника `.bsl` — тогда `-ConfigPath` указывать не нужно (оригинал читается прямо из этого файла). Путь модуля расширения определяется от типовой папки в пути автоматически.
## Типы перехвата ## Типы перехвата
| InterceptorType | Декоратор | Назначение | | InterceptorType | Декоратор | Назначение | Применим к |
|-----------------|-----------|------------| |-----------------|-----------|------------|------------|
| `Before` | `&Перед` | Код до вызова оригинального метода | | `Before` | `&Перед` | Код до вызова оригинального метода | процедуры |
| `After` | `&После` | Код после вызова оригинального метода | | `After` | `&После` | Код после вызова оригинального метода | процедуры |
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` | | `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
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 ```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 ```
&НаСервере /cfe-validate <ExtensionPath>
&Перед("ПриЗаписи")
Процедура Расш1_ПриЗаписи()
// TODO: код перед вызовом оригинального метода
КонецПроцедуры
``` ```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -930,6 +930,17 @@ if ($script:borrowedFormsWithTree.Count -eq 0) {
Report-OK "13. TypeLink: clean" 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 --- # --- Final output ---
& $finalize & $finalize
@@ -885,6 +885,21 @@ def main():
elif check13_ok: elif check13_ok:
r.ok('13. TypeLink: clean') r.ok('13. TypeLink: clean')
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
ctrl_count = 0
for dp, _dn, files in os.walk(config_dir):
for fn in files:
if fn.endswith('.bsl'):
try:
with open(os.path.join(dp, fn), 'r', encoding='utf-8-sig') as f:
for ln in f:
if re.match(r'^\s*&ИзменениеИКонтроль\(', ln):
ctrl_count += 1
except OSError:
pass
if ctrl_count > 0:
r.out('[INFO] Контролируемых методов (&ИзменениеИКонтроль): %d — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>' % ctrl_count)
# --- Final output --- # --- Final output ---
r.finalize(out_file) r.finalize(out_file)
sys.exit(1 if r.errors > 0 else 0) sys.exit(1 if r.errors > 0 else 0)
+2 -2
View File
@@ -25,7 +25,7 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе). Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
После создания базы предложи зарегистрировать через `/db-list add`. После создания базы предложи зарегистрировать через `/db-list add`.
## Команда ## Команда
@@ -38,7 +38,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Путь к файловой базе | | `-InfoBasePath <путь>` | * | Путь к файловой базе |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
+83 -4
View File
@@ -1,5 +1,6 @@
# db-create v1.1 — Create 1C information base # db-create v1.7 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Создание информационной базы 1С Создание информационной базы 1С
@@ -96,7 +97,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -105,12 +106,55 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- 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)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | 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()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
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" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1 exit 1
} }
@@ -126,6 +170,35 @@ $tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
$arguments = @("infobase", "create", "--db-path=$InfoBasePath", "--create-database")
if ($UseTemplate) {
if ([System.IO.Path]::GetExtension($UseTemplate) -ieq ".dt") {
$arguments += "--restore=$UseTemplate"
} else {
$arguments += "--load=$UseTemplate", "--apply"
}
}
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $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) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("CREATEINFOBASE") $arguments = @("CREATEINFOBASE")
@@ -160,12 +233,18 @@ try {
$exitCode = $process.ExitCode $exitCode = $process.ExitCode
# --- Result --- # --- 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 ($exitCode -eq 0) {
if ($InfoBaseServer -and $InfoBaseRef) { if ($InfoBaseServer -and $InfoBaseRef) {
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
} else { } else {
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green 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 { } else {
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
} }
+111 -18
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-create v1.1 — Create 1C information base # db-create v1.7 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,76 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
def file_ib_created(ib_path):
"""File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
Exit code 0 without it (broken/headless env) is a false success reject it."""
f = os.path.join(ib_path, "1Cv8.1CD")
return os.path.isfile(f) and os.path.getsize(f) > 0
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -82,9 +123,14 @@ def main():
args = parser.parse_args() args = parser.parse_args()
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -93,6 +139,39 @@ def main():
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr) print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
arguments = ["infobase", "create", f"--db-path={args.InfoBasePath}", "--create-database"]
if args.UseTemplate:
if os.path.splitext(args.UseTemplate)[1].lower() == ".dt":
arguments.append(f"--restore={args.UseTemplate}")
else:
arguments.extend([f"--load={args.UseTemplate}", "--apply"])
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
exit_code = result.returncode
ib_missing = exit_code == 0 and not file_ib_created(args.InfoBasePath)
if ib_missing:
exit_code = 1
if exit_code == 0:
print(f"Information base created successfully: {args.InfoBasePath}")
elif ib_missing:
print(
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
"— information base was not created",
file=sys.stderr,
)
else:
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -102,9 +181,11 @@ def main():
arguments = ["CREATEINFOBASE"] arguments = ["CREATEINFOBASE"]
if args.InfoBaseServer and args.InfoBaseRef: if args.InfoBaseServer and args.InfoBaseRef:
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"') # No embedded quotes: subprocess quotes the whole token; 1C's argv parser
# strips outer quotes. Inner quotes get escaped by list2cmdline and break parsing.
arguments.append(f'Srvr={args.InfoBaseServer};Ref={args.InfoBaseRef}')
else: else:
arguments.append(f'File="{args.InfoBasePath}"') arguments.append(f'File={args.InfoBasePath}')
# --- Template --- # --- Template ---
if args.UseTemplate: if args.UseTemplate:
@@ -132,11 +213,23 @@ def main():
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- Result ---
# Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
is_server = bool(args.InfoBaseServer and args.InfoBaseRef)
ib_missing = exit_code == 0 and not is_server and not file_ib_created(args.InfoBasePath)
if ib_missing:
exit_code = 1
if exit_code == 0: if exit_code == 0:
if args.InfoBaseServer and args.InfoBaseRef: if is_server:
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}") print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
else: else:
print(f"Information base created successfully: {args.InfoBasePath}") print(f"Information base created successfully: {args.InfoBasePath}")
elif ib_missing:
print(
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
"— information base was not created",
file=sys.stderr,
)
else: else:
print(f"Error creating information base (code: {exit_code})", file=sys.stderr) print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
+2 -2
View File
@@ -28,7 +28,7 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
@@ -42,7 +42,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -1,5 +1,6 @@
# db-dump-cf v1.1 — Dump 1C configuration to CF file # db-dump-cf v1.9 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Выгрузка конфигурации 1С в CF-файл Выгрузка конфигурации 1С в CF-файл
@@ -75,6 +76,13 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::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
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
$dir = (Get-Location).Path $dir = (Get-Location).Path
@@ -105,7 +113,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -114,12 +122,54 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- 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)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | 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()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
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" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1 exit 1
} }
@@ -135,6 +185,36 @@ $tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
if ($AllExtensions) {
Write-Host "Error: ibcmd config save does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "save", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$OutputFile"
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $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) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
@@ -162,13 +242,18 @@ try {
$arguments += "/DisableStartupDialogs" $arguments += "/DisableStartupDialogs"
# --- Execute --- # --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode $exitCode = $process.ExitCode
# --- Result --- # --- 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) { if ($exitCode -eq 0) {
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green 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 { } else {
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
} }
+110 -16
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-cf v1.1 — Dump 1C configuration to CF file # db-dump-cf v1.9 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,84 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def output_nonempty(path):
"""Postcondition: the platform must have produced a non-empty output file.
Exit code 0 without it (broken/headless env) is a false success reject it."""
return os.path.isfile(path) and os.path.getsize(path) > 0
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -84,9 +133,14 @@ def main():
args = parser.parse_args() args = parser.parse_args()
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -95,6 +149,40 @@ def main():
if out_dir and not os.path.isdir(out_dir): if out_dir and not os.path.isdir(out_dir):
os.makedirs(out_dir, exist_ok=True) os.makedirs(out_dir, exist_ok=True)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
if args.AllExtensions:
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.OutputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
exit_code = result.returncode
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Configuration dumped successfully to: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -127,7 +215,7 @@ def main():
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
# --- Execute --- # --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}") print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = subprocess.run( result = subprocess.run(
[v8path] + arguments, [v8path] + arguments,
capture_output=True, capture_output=True,
@@ -136,8 +224,14 @@ def main():
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0: if exit_code == 0:
print(f"Configuration dumped successfully to: {args.OutputFile}") print(f"Configuration dumped successfully to: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
else: else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr) print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
+2 -2
View File
@@ -31,7 +31,7 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
@@ -45,7 +45,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -1,5 +1,6 @@
# db-dump-dt v1.1 — Dump 1C information base to DT file # db-dump-dt v1.8 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Выгрузка информационной базы 1С в DT-файл Выгрузка информационной базы 1С в DT-файл
@@ -59,6 +60,13 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::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
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
$dir = (Get-Location).Path $dir = (Get-Location).Path
@@ -89,7 +97,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -98,12 +106,54 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- 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)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | 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()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
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" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1 exit 1
} }
@@ -119,6 +169,32 @@ $tempDir = Join-Path $env:TEMP "db_dump_dt_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
$arguments = @("infobase", "dump", "--db-path=$InfoBasePath")
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "$OutputFile"
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $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) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
@@ -139,13 +215,18 @@ try {
$arguments += "/DisableStartupDialogs" $arguments += "/DisableStartupDialogs"
# --- Execute --- # --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode $exitCode = $process.ExitCode
# --- Result --- # --- 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) { if ($exitCode -eq 0) {
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green 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 { } else {
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
} }
+105 -16
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-dt v1.1 — Dump 1C information base to DT file # db-dump-dt v1.8 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,84 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def output_nonempty(path):
"""Postcondition: the platform must have produced a non-empty output file.
Exit code 0 without it (broken/headless env) is a false success reject it."""
return os.path.isfile(path) and os.path.getsize(path) > 0
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -82,9 +131,14 @@ def main():
args = parser.parse_args() args = parser.parse_args()
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -93,6 +147,35 @@ def main():
if out_dir and not os.path.isdir(out_dir): if out_dir and not os.path.isdir(out_dir):
os.makedirs(out_dir, exist_ok=True) os.makedirs(out_dir, exist_ok=True)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
arguments = ["infobase", "dump", f"--db-path={args.InfoBasePath}"]
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(args.OutputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
exit_code = result.returncode
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Information base dumped successfully to: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
else:
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -119,7 +202,7 @@ def main():
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
# --- Execute --- # --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}") print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = subprocess.run( result = subprocess.run(
[v8path] + arguments, [v8path] + arguments,
capture_output=True, capture_output=True,
@@ -128,8 +211,14 @@ def main():
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0: if exit_code == 0:
print(f"Information base dumped successfully to: {args.OutputFile}") 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: else:
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr) print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
+2 -2
View File
@@ -29,7 +29,7 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию. Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
@@ -44,7 +44,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -1,5 +1,6 @@
# db-dump-xml v1.1 — Dump 1C configuration to XML files # db-dump-xml v1.11 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Выгрузка конфигурации 1С в XML-файлы Выгрузка конфигурации 1С в XML-файлы
@@ -98,6 +99,13 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::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
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
$dir = (Get-Location).Path $dir = (Get-Location).Path
@@ -128,7 +136,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -137,12 +145,54 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- 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)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | 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()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
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" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1 exit 1
} }
@@ -164,6 +214,48 @@ $tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
if ($AllExtensions) {
$arguments = @("infobase", "config", "export", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
} elseif ($Mode -eq "UpdateInfo") {
Write-Host "Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8" -ForegroundColor Red
exit 1
} elseif ($Mode -eq "Partial") {
$objList = @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
$arguments = @("infobase", "config", "export", "objects") + $objList
$arguments += "--out=$ConfigDir", "--db-path=$InfoBasePath"
if ($Extension) { $arguments += "--extension=$Extension" }
} else {
$arguments = @("infobase", "config", "export", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$ConfigDir"
}
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $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) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
@@ -219,14 +311,19 @@ try {
$arguments += "/DisableStartupDialogs" $arguments += "/DisableStartupDialogs"
# --- Execute --- # --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode $exitCode = $process.ExitCode
# --- Result --- # --- 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) { if ($exitCode -eq 0) {
Write-Host "Dump completed successfully" -ForegroundColor Green Write-Host "Dump completed successfully" -ForegroundColor Green
Write-Host "Configuration dumped to: $ConfigDir" 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 { } else {
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
} }
+122 -16
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-xml v1.1 — Dump 1C configuration to XML files # db-dump-xml v1.11 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,84 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def dir_nonempty(path):
"""Postcondition: the platform must have written files into the output directory.
Exit code 0 with an empty dir (broken/headless env) is a false success reject it."""
return os.path.isdir(path) and any(os.scandir(path))
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -98,9 +147,14 @@ def main():
# --- Resolve V8Path --- # --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -114,6 +168,52 @@ def main():
os.makedirs(args.ConfigDir, exist_ok=True) os.makedirs(args.ConfigDir, exist_ok=True)
print(f"Created output directory: {args.ConfigDir}") print(f"Created output directory: {args.ConfigDir}")
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
if engine == "ibcmd":
if args.Format == "Plain":
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
if args.AllExtensions:
arguments = ["infobase", "config", "export", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
elif args.Mode == "UpdateInfo":
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8", file=sys.stderr)
sys.exit(1)
elif args.Mode == "Partial":
obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()]
arguments = ["infobase", "config", "export", "objects"] + obj_list
arguments += [f"--out={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
else:
arguments = ["infobase", "config", "export", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.ConfigDir)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
exit_code = result.returncode
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Configuration exported successfully to: {args.ConfigDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported", file=sys.stderr)
else:
print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -169,7 +269,7 @@ def main():
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
# --- Execute --- # --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}") print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = subprocess.run( result = subprocess.run(
[v8path] + arguments, [v8path] + arguments,
capture_output=True, capture_output=True,
@@ -178,9 +278,15 @@ def main():
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- Result ---
# Postcondition: exit 0 with an empty output directory is a false success.
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
if out_missing:
exit_code = 1
if exit_code == 0: if exit_code == 0:
print("Dump completed successfully") print("Dump completed successfully")
print(f"Configuration dumped to: {args.ConfigDir}") print(f"Configuration dumped to: {args.ConfigDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped", file=sys.stderr)
else: else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr) print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
+2 -2
View File
@@ -29,7 +29,7 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
@@ -43,7 +43,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -1,5 +1,6 @@
# db-load-cf v1.1 — Load 1C configuration from CF file # db-load-cf v1.10 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Загрузка конфигурации 1С из CF-файла Загрузка конфигурации 1С из CF-файла
@@ -75,6 +76,30 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::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 ""
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
$dir = (Get-Location).Path $dir = (Get-Location).Path
@@ -105,7 +130,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -114,12 +139,47 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- 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)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | 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()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1 exit 1
} }
@@ -135,6 +195,32 @@ $tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
if ($AllExtensions) {
Write-Host "Error: ibcmd config load does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "load", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$InputFile"
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
@@ -162,7 +248,7 @@ try {
$arguments += "/DisableStartupDialogs" $arguments += "/DisableStartupDialogs"
# --- Execute --- # --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode $exitCode = $process.ExitCode
@@ -170,7 +256,7 @@ try {
if ($exitCode -eq 0) { if ($exitCode -eq 0) {
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
} else { } 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) { if (Test-Path $outFile) {
+117 -17
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-cf v1.1 — Load 1C configuration from CF file # db-load-cf v1.10 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,102 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -84,9 +151,14 @@ def main():
args = parser.parse_args() args = parser.parse_args()
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -95,6 +167,34 @@ def main():
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr) print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
if args.AllExtensions:
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.InputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
print(f"Configuration loaded successfully from: {args.InputFile}")
else:
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -127,7 +227,7 @@ def main():
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
# --- Execute --- # --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}") print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = subprocess.run( result = subprocess.run(
[v8path] + arguments, [v8path] + arguments,
capture_output=True, capture_output=True,
@@ -139,7 +239,7 @@ def main():
if exit_code == 0: if exit_code == 0:
print(f"Configuration loaded successfully from: {args.InputFile}") print(f"Configuration loaded successfully from: {args.InputFile}")
else: else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr) print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if os.path.isfile(out_file): if os.path.isfile(out_file):
try: try:
+2 -2
View File
@@ -45,7 +45,7 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
@@ -59,7 +59,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -1,5 +1,6 @@
# db-load-dt v1.1 — Load 1C information base from DT file # db-load-dt v1.9 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Загрузка информационной базы 1С из DT-файла Загрузка информационной базы 1С из DT-файла
@@ -72,6 +73,30 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::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 ""
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
$dir = (Get-Location).Path $dir = (Get-Location).Path
@@ -102,7 +127,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -111,12 +136,47 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- 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)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | 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()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1 exit 1
} }
@@ -132,6 +192,29 @@ $tempDir = Join-Path $env:TEMP "db_load_dt_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
$arguments = @("infobase", "restore", "--db-path=$InfoBasePath")
if (-not (Test-Path (Join-Path $InfoBasePath "1Cv8.1CD"))) { $arguments += "--create-database" }
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "$InputFile"
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
@@ -154,7 +237,7 @@ try {
$arguments += "/DisableStartupDialogs" $arguments += "/DisableStartupDialogs"
# --- Execute --- # --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode $exitCode = $process.ExitCode
@@ -162,7 +245,7 @@ try {
if ($exitCode -eq 0) { if ($exitCode -eq 0) {
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
} else { } 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) { if (Test-Path $outFile) {
+114 -17
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-dt v1.1 — Load 1C information base from DT file # db-load-dt v1.9 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,102 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -84,9 +151,14 @@ def main():
args = parser.parse_args() args = parser.parse_args()
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -95,6 +167,31 @@ def main():
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr) print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
arguments = ["infobase", "restore", f"--db-path={args.InfoBasePath}"]
if not os.path.isfile(os.path.join(args.InfoBasePath, "1Cv8.1CD")):
arguments.append("--create-database")
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(args.InputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
print(f"Information base restored successfully from: {args.InputFile}")
else:
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -125,7 +222,7 @@ def main():
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
# --- Execute --- # --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}") print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = subprocess.run( result = subprocess.run(
[v8path] + arguments, [v8path] + arguments,
capture_output=True, capture_output=True,
@@ -137,7 +234,7 @@ def main():
if exit_code == 0: if exit_code == 0:
print(f"Information base restored successfully from: {args.InputFile}") print(f"Information base restored successfully from: {args.InputFile}")
else: 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): if os.path.isfile(out_file):
try: try:
+2 -2
View File
@@ -30,7 +30,7 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог конфигурации. Если в записи базы указан `configSrc` — используй как каталог конфигурации.
@@ -45,7 +45,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -1,5 +1,6 @@
# db-load-git v1.5 — Load Git changes into 1C database # db-load-git v1.15 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Загрузка изменений из Git в базу 1С Загрузка изменений из Git в базу 1С
@@ -107,6 +108,30 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::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 ""
}
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML --- # --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
function Get-ObjectXmlFromSubFile { function Get-ObjectXmlFromSubFile {
param([string]$RelativePath) param([string]$RelativePath)
@@ -149,7 +174,7 @@ if (-not $DryRun) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -158,14 +183,48 @@ if (-not $DryRun) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
# --- Validate connection (skip if DryRun) --- # --- Detect engine + validate connection (skip if DryRun) ---
$engine = "1cv8"
if (-not $DryRun) { if (-not $DryRun) {
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { 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)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | 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()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1 exit 1
} }
@@ -192,6 +251,15 @@ try {
} }
# --- Get changed files from Git --- # --- Get changed files from Git ---
# Все git-вызовы для сбора путей идут через один хелпер с -c core.quotePath=false,
# иначе кириллические пути возвращаются в octal-виде и не распознаются (зеркало run_git в .py).
function Invoke-GitLines {
param([string[]]$GitArgs)
$out = git -c core.quotePath=false @GitArgs 2>&1
if ($LASTEXITCODE -eq 0) { return $out }
return @()
}
$changedFiles = @() $changedFiles = @()
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\') $ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
$configDirNormalized = $ConfigDir.Replace('\', '/') $configDirNormalized = $ConfigDir.Replace('\', '/')
@@ -201,29 +269,22 @@ try {
switch ($Source) { switch ($Source) {
"Staged" { "Staged" {
Write-Host "Getting staged changes..." Write-Host "Getting staged changes..."
$raw = git diff --cached --name-only --relative 2>&1 $changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
} }
"Unstaged" { "Unstaged" {
Write-Host "Getting unstaged changes..." Write-Host "Getting unstaged changes..."
$raw = git diff --name-only --relative 2>&1 $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } $changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
$raw = git ls-files --others --exclude-standard 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
} }
"Commit" { "Commit" {
Write-Host "Getting changes from $CommitRange..." Write-Host "Getting changes from $CommitRange..."
$raw = git diff --name-only --relative $CommitRange 2>&1 $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative', $CommitRange)
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
} }
"All" { "All" {
Write-Host "Getting all uncommitted changes..." Write-Host "Getting all uncommitted changes..."
$raw = git diff --cached --name-only --relative 2>&1 $changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
$raw = git diff --name-only --relative 2>&1 $changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
$raw = git ls-files --others --exclude-standard 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
} }
} }
} finally { } finally {
@@ -319,6 +380,53 @@ $tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only; import specific files) ---
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
if ($AllExtensions) {
Write-Host "Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "import", "files") + $configFiles
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
if ($Extension) { $arguments += "--extension=$Extension" }
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -ne 0) {
Write-Host "Error loading changes (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
if ($output) { Write-Host ($output | Out-String) }
if ($UpdateDB) {
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
if ($UserName) { $applyArgs += "--user=$UserName" }
if ($Password) { $applyArgs += "--password=$Password" }
$applyArgs += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($applyArgs -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
$applyOut = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($applyOut) { Write-Host ($applyOut | Out-String) }
}
exit $exitCode
}
# --- 1cv8 branch ---
# --- Write list file (UTF-8 with BOM) --- # --- Write list file (UTF-8 with BOM) ---
$listFile = Join-Path $tempDir "load_list.txt" $listFile = Join-Path $tempDir "load_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true) $utf8Bom = New-Object System.Text.UTF8Encoding($true)
@@ -362,7 +470,7 @@ try {
# --- Execute --- # --- Execute ---
Write-Host "" Write-Host ""
Write-Host "Executing partial configuration load..." Write-Host "Executing partial configuration load..."
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode $exitCode = $process.ExitCode
@@ -372,7 +480,7 @@ try {
if ($exitCode -eq 0) { if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green Write-Host "Load completed successfully" -ForegroundColor Green
} else { } 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) { if (Test-Path $outFile) {
+144 -19
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-git v1.5 — Load Git changes into 1C database # db-load-git v1.15 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,69 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def get_object_xml_from_subfile(relative_path): def get_object_xml_from_subfile(relative_path):
"""Map sub-file path (BSL, HTML, etc.) to object XML path.""" """Map sub-file path (BSL, HTML, etc.) to object XML path."""
parts = re.split(r"[\\/]", relative_path) parts = re.split(r"[\\/]", relative_path)
@@ -76,7 +110,7 @@ def get_object_xml_from_subfile(relative_path):
def run_git(config_dir, git_args): def run_git(config_dir, git_args):
"""Run a git command in config_dir and return output lines on success.""" """Run a git command in config_dir and return output lines on success."""
result = subprocess.run( result = subprocess.run(
["git"] + git_args, ["git", "-c", "core.quotePath=false"] + git_args,
capture_output=True, capture_output=True,
text=True, text=True,
encoding="utf-8", encoding="utf-8",
@@ -87,6 +121,39 @@ def run_git(config_dir, git_args):
return [] return []
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -125,9 +192,15 @@ def main():
if not args.DryRun: if not args.DryRun:
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
# --- Validate connection (skip if DryRun) --- # --- Detect engine + validate connection (skip if DryRun) ---
engine = "1cv8"
if not args.DryRun: if not args.DryRun:
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -248,6 +321,58 @@ def main():
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
try: try:
if engine == "ibcmd":
# --- ibcmd branch (file infobase only; import specific files) ---
if args.Format == "Plain":
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
if args.AllExtensions:
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "import", "files"] + config_files
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode != 0:
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
print(f"Changes loaded successfully ({len(config_files)} files)")
if result.stdout:
print(result.stdout)
exit_code = 0
if args.UpdateDB:
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
if args.UserName:
apply_args.append(f"--user={args.UserName}")
if args.Password:
apply_args.append(f"--password={args.Password}")
apply_args.append(f"--data={ib_data}")
print(f"Running: ibcmd {_redact(' '.join(apply_args), args.Password, args.UserName)}")
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
exit_code = ar.returncode
if exit_code == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if ar.stdout:
print(ar.stdout)
if ar.stderr:
print(ar.stderr, file=sys.stderr)
sys.exit(exit_code)
# --- Write list file (UTF-8 with BOM) --- # --- Write list file (UTF-8 with BOM) ---
list_file = os.path.join(temp_dir, "load_list.txt") list_file = os.path.join(temp_dir, "load_list.txt")
with open(list_file, "w", encoding="utf-8-sig") as f: with open(list_file, "w", encoding="utf-8-sig") as f:
@@ -290,7 +415,7 @@ def main():
# --- Execute --- # --- Execute ---
print("") print("")
print("Executing partial configuration load...") print("Executing partial configuration load...")
print(f"Running: 1cv8.exe {' '.join(arguments)}") print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = subprocess.run( result = subprocess.run(
[v8path] + arguments, [v8path] + arguments,
@@ -304,7 +429,7 @@ def main():
if exit_code == 0: if exit_code == 0:
print("Load completed successfully") print("Load completed successfully")
else: else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr) print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if os.path.isfile(out_file): if os.path.isfile(out_file):
try: try:
+2 -2
View File
@@ -30,7 +30,7 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию. Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
@@ -45,7 +45,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -1,5 +1,6 @@
# db-load-xml v1.5 — Load 1C configuration from XML files # db-load-xml v1.16 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Загрузка конфигурации 1С из XML-файлов Загрузка конфигурации 1С из XML-файлов
@@ -107,6 +108,30 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::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 ""
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
$dir = (Get-Location).Path $dir = (Get-Location).Path
@@ -137,7 +162,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -146,12 +171,47 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- 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)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | 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()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1 exit 1
} }
@@ -173,6 +233,73 @@ $tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
if ($AllExtensions) {
$arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
} elseif ($Mode -eq "Partial" -or $Files -or $ListFile) {
# partial: import specific files (relative to ConfigDir)
$fileList = @()
if ($ListFile) {
if (-not (Test-Path $ListFile)) {
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
exit 1
}
$fileList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
} elseif ($Files) {
$fileList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
if ($fileList.Count -eq 0) {
Write-Host "Error: -Files or -ListFile required for partial import" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "import", "files") + $fileList
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
if ($Extension) { $arguments += "--extension=$Extension" }
} else {
$arguments = @("infobase", "config", "import", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$ConfigDir"
}
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -ne 0) {
Write-Host "Error loading configuration from files (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
if ($output) { Write-Host ($output | Out-String) }
if ($UpdateDB) {
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
if ($UserName) { $applyArgs += "--user=$UserName" }
if ($Password) { $applyArgs += "--password=$Password" }
$applyArgs += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($applyArgs -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
$applyOut = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($applyOut) { Write-Host ($applyOut | Out-String) }
}
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
@@ -248,7 +375,7 @@ try {
$arguments += "/DisableStartupDialogs" $arguments += "/DisableStartupDialogs"
# --- Execute --- # --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode $exitCode = $process.ExitCode
@@ -289,7 +416,7 @@ try {
if ($exitCode -eq 0) { if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green Write-Host "Load completed successfully" -ForegroundColor Green
} else { } else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
} }
if ($logContent) { if ($logContent) {
+161 -17
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-xml v1.5 — Load 1C configuration from XML files # db-load-xml v1.16 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,102 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -106,8 +173,14 @@ def main():
# --- Resolve V8Path --- # --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -121,6 +194,77 @@ def main():
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr) print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
if engine == "ibcmd":
if args.Format == "Plain":
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
if args.AllExtensions:
arguments = ["infobase", "config", "import", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
elif args.Mode == "Partial" or args.Files or args.ListFile:
# partial: import specific files (relative to ConfigDir)
if args.ListFile:
if not os.path.isfile(args.ListFile):
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
sys.exit(1)
with open(args.ListFile, encoding="utf-8-sig") as f:
file_list = [ln.strip() for ln in f if ln.strip()]
elif args.Files:
file_list = [p.strip() for p in args.Files.split(",") if p.strip()]
else:
file_list = []
if not file_list:
print("Error: -Files or -ListFile required for partial import", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "import", "files"] + file_list
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
else:
arguments = ["infobase", "config", "import", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.ConfigDir)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode != 0:
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
print(f"Configuration loaded successfully from: {args.ConfigDir}")
if result.stdout:
print(result.stdout)
exit_code = 0
if args.UpdateDB:
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
if args.UserName:
apply_args.append(f"--user={args.UserName}")
if args.Password:
apply_args.append(f"--password={args.Password}")
apply_args.append(f"--data={ib_data}")
print(f"Running: ibcmd {_redact(' '.join(apply_args), args.Password, args.UserName)}")
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
exit_code = ar.returncode
if exit_code == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if ar.stdout:
print(ar.stdout)
if ar.stderr:
print(ar.stderr, file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -197,7 +341,7 @@ def main():
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
# --- Execute --- # --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}") print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = subprocess.run( result = subprocess.run(
[v8path] + arguments, [v8path] + arguments,
capture_output=True, capture_output=True,
@@ -241,7 +385,7 @@ def main():
if exit_code == 0: if exit_code == 0:
print("Load completed successfully") print("Load completed successfully")
else: else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr) print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if log_content: if log_content:
print("--- Log ---") print("--- Log ---")
+1 -1
View File
@@ -29,7 +29,7 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
+30 -6
View File
@@ -1,5 +1,6 @@
# db-run v1.1 — Launch 1C:Enterprise # db-run v1.4 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Запуск 1С:Предприятие Запуск 1С:Предприятие
@@ -78,6 +79,13 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::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
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
$dir = (Get-Location).Path $dir = (Get-Location).Path
@@ -108,7 +116,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -117,7 +125,7 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
@@ -164,7 +172,23 @@ if ($URL) {
$argString += " /DisableStartupDialogs" $argString += " /DisableStartupDialogs"
# --- Execute (background, no wait) --- # --- Execute (background) ---
Write-Host "Running: 1cv8.exe $argString" # Redact the password/user before printing the command line — never leak secrets.
Start-Process -FilePath $V8Path -ArgumentList $argString $displayArg = Protect-Secrets $argString @($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 Write-Host "1C:Enterprise launched" -ForegroundColor Green
+53 -17
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-run v1.1 — Launch 1C:Enterprise # db-run v1.4 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -9,6 +9,7 @@ import os
import re import re
import subprocess import subprocess
import sys import sys
import time
def _find_project_v8path(): def _find_project_v8path():
@@ -32,36 +33,57 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -119,9 +141,23 @@ def main():
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
# --- Execute (background, no wait) --- # --- Execute (background) ---
print(f"Running: 1cv8.exe {' '.join(arguments)}") # Redact the password/user before printing the command line — never leak secrets.
subprocess.Popen([v8path] + arguments) print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
proc = subprocess.Popen([v8path] + arguments)
# --- Bounded early-exit check ---
# The launch is a background GUI process, so we don't wait for completion. But a process
# that dies within the first ~1.5s never really started (bad base, no display, license) —
# report that honestly instead of a blind "launched".
deadline = time.monotonic() + 1.5
while time.monotonic() < deadline and proc.poll() is None:
time.sleep(0.2)
rc = proc.poll()
if rc is not None:
print(f"Error: 1C:Enterprise exited immediately (code: {rc})", file=sys.stderr)
sys.exit(rc if rc and rc > 0 else 1)
print(f"PID: {proc.pid}")
print("1C:Enterprise launched") print("1C:Enterprise launched")
+2 -2
View File
@@ -28,7 +28,7 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
@@ -42,7 +42,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
+93 -6
View File
@@ -1,5 +1,6 @@
# db-update v1.1 — Update 1C database configuration # db-update v1.10 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Обновление конфигурации базы данных 1С Обновление конфигурации базы данных 1С
@@ -88,6 +89,30 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::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 ""
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
$dir = (Get-Location).Path $dir = (Get-Location).Path
@@ -118,7 +143,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -127,12 +152,47 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- 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)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | 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()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1 exit 1
} }
@@ -142,6 +202,33 @@ $tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
if ($AllExtensions) {
Write-Host "Error: ibcmd config apply does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
if ($Dynamic -eq "+") { $arguments += "--dynamic=auto" }
elseif ($Dynamic -eq "-") { $arguments += "--dynamic=disable" }
if ($Extension) { $arguments += "--extension=$Extension" }
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
@@ -180,7 +267,7 @@ try {
$arguments += "/DisableStartupDialogs" $arguments += "/DisableStartupDialogs"
# --- Execute --- # --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode $exitCode = $process.ExitCode
@@ -188,7 +275,7 @@ try {
if ($exitCode -eq 0) { if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else { } 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) { if (Test-Path $outFile) {
+121 -17
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-update v1.1 — Update 1C database configuration # db-update v1.10 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,102 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -87,11 +154,48 @@ def main():
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
if args.AllExtensions:
print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
if args.Dynamic == "+":
arguments.append("--dynamic=auto")
elif args.Dynamic == "-":
arguments.append("--dynamic=disable")
if args.Extension:
arguments.append(f"--extension={args.Extension}")
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -132,7 +236,7 @@ def main():
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
# --- Execute --- # --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}") print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = subprocess.run( result = subprocess.run(
[v8path] + arguments, [v8path] + arguments,
capture_output=True, capture_output=True,
@@ -144,7 +248,7 @@ def main():
if exit_code == 0: if exit_code == 0:
print("Database configuration updated successfully") print("Database configuration updated successfully")
else: else:
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr) print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if os.path.isfile(out_file): if os.path.isfile(out_file):
try: try:
+2 -2
View File
@@ -34,7 +34,7 @@ allowed-tools:
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. 6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
@@ -47,7 +47,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
+83 -4
View File
@@ -1,5 +1,6 @@
# epf-build v1.1 — Build external data processor or report (EPF/ERF) from XML sources # epf-build v1.9 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Сборка внешней обработки/отчёта 1С из XML-исходников Сборка внешней обработки/отчёта 1С из XML-исходников
@@ -69,6 +70,13 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::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
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
$dir = (Get-Location).Path $dir = (Get-Location).Path
@@ -99,7 +107,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -108,7 +116,48 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- 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)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | 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()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
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" }
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 exit 1
} }
@@ -146,6 +195,31 @@ $tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch: build EPF/ERF via config import --out ---
$srcDir = Split-Path $SourceFile -Parent
$arguments = @("infobase", "config", "import", "$srcDir", "--out=$OutputFile", "--db-path=$InfoBasePath")
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $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) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
@@ -166,13 +240,18 @@ try {
$arguments += "/DisableStartupDialogs" $arguments += "/DisableStartupDialogs"
# --- Execute --- # --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode $exitCode = $process.ExitCode
# --- Result --- # --- 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) { if ($exitCode -eq 0) {
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green 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 { } else {
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red Write-Host "Error building (code: $exitCode)" -ForegroundColor Red
} }
+103 -15
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-build v1.1 — Build external data processor or report (EPF/ERF) from XML sources # epf-build v1.9 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,84 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def output_nonempty(path):
"""Postcondition: the platform must have produced a non-empty output file.
Exit code 0 without it (broken/headless env) is a false success reject it."""
return os.path.isfile(path) and os.path.getsize(path) > 0
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -84,6 +133,10 @@ def main():
# --- Resolve V8Path --- # --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
if engine == "ibcmd" and args.InfoBaseServer and args.InfoBaseRef:
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)", file=sys.stderr)
sys.exit(1)
# --- Auto-create stub database if no connection specified --- # --- Auto-create stub database if no connection specified ---
auto_created_base = None auto_created_base = None
@@ -117,6 +170,35 @@ def main():
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
try: try:
if engine == "ibcmd":
# --- ibcmd branch: build EPF/ERF via config import --out ---
src_dir = os.path.dirname(os.path.abspath(args.SourceFile))
arguments = ["infobase", "config", "import", src_dir, f"--out={args.OutputFile}", f"--db-path={args.InfoBasePath}"]
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
exit_code = result.returncode
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"External data processor/report built successfully: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
else:
print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(exit_code)
# --- Build arguments --- # --- Build arguments ---
arguments = ["DESIGNER"] arguments = ["DESIGNER"]
@@ -138,7 +220,7 @@ def main():
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
# --- Execute --- # --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}") print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = subprocess.run( result = subprocess.run(
[v8path] + arguments, [v8path] + arguments,
capture_output=True, capture_output=True,
@@ -147,8 +229,14 @@ def main():
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0: if exit_code == 0:
print(f"Build completed successfully: {args.OutputFile}") print(f"Build completed successfully: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
else: else:
print(f"Error building (code: {exit_code})", file=sys.stderr) print(f"Error building (code: {exit_code})", file=sys.stderr)
@@ -1,4 +1,4 @@
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build # stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -1252,6 +1252,57 @@ $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)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | 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()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($stubEngine -eq "ibcmd") {
Write-Host "Creating infobase (ibcmd): $TempBasePath"
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
New-Item -ItemType Directory -Path $ibData -Force | Out-Null
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
$ibArgs += "--data=$ibData"
$__ib = Invoke-IbcmdProcess $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-Error "Failed to create stub infobase (code: $ibRc)"
exit 1
}
if ($hasRefTypes) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
Write-Host "[OK] Stub database created: $TempBasePath"
Write-Host $TempBasePath
exit 0
}
# --- 5. Create infobase --- # --- 5. Create infobase ---
Write-Host "Creating infobase: $TempBasePath" Write-Host "Creating infobase: $TempBasePath"
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs" $createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build # stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -12,6 +12,27 @@ import tempfile
import uuid import uuid
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def new_uuid(): def new_uuid():
return str(uuid.uuid4()) return str(uuid.uuid4())
@@ -1034,6 +1055,32 @@ def main():
if register_columns: if register_columns:
print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.') print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.')
# Stub via ibcmd (one call: create [--import --apply])
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
if stub_engine == "ibcmd":
import shutil
print(f'Creating infobase (ibcmd): {temp_base}')
ib_data = tempfile.mkdtemp(prefix="stub_data_")
ib_args = [args.V8Path, 'infobase', 'create', f'--db-path={temp_base}', '--create-database']
if has_ref_types:
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
ib_args.append(f'--data={ib_data}')
result = run_ibcmd(ib_args, warn_no_user=False)
shutil.rmtree(ib_data, ignore_errors=True)
if result.returncode != 0:
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
print(f'Failed to create stub infobase (code: {result.returncode})', file=sys.stderr)
sys.exit(1)
if has_ref_types:
import shutil
shutil.rmtree(os.path.join(temp_base, 'cfg'), ignore_errors=True)
print(f'[OK] Stub database created: {temp_base}')
print(temp_base)
sys.exit(0)
# Create infobase # Create infobase
print(f'Creating infobase: {temp_base}') print(f'Creating infobase: {temp_base}')
result = subprocess.run( result = subprocess.run(
+2 -2
View File
@@ -33,7 +33,7 @@ allowed-tools:
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. 6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
@@ -46,7 +46,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <па
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
+88 -4
View File
@@ -1,5 +1,6 @@
# epf-dump v1.1 — Dump external data processor or report (EPF/ERF) to XML sources # epf-dump v1.8 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Разборка внешней обработки/отчёта 1С в XML-исходники Разборка внешней обработки/отчёта 1С в XML-исходники
@@ -106,7 +107,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -115,7 +116,7 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
@@ -126,6 +127,60 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
exit 1 exit 1
} }
# --- 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)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | 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()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
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" }
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
}
# --- Validate input file --- # --- Validate input file ---
if (-not (Test-Path $InputFile)) { if (-not (Test-Path $InputFile)) {
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
@@ -142,6 +197,30 @@ $tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch: dump EPF/ERF via config export --file ---
$arguments = @("infobase", "config", "export", "--file=$InputFile", "$OutputDir", "--db-path=$InfoBasePath")
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $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) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
@@ -163,13 +242,18 @@ try {
$arguments += "/DisableStartupDialogs" $arguments += "/DisableStartupDialogs"
# --- Execute --- # --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode $exitCode = $process.ExitCode
# --- Result --- # --- 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) { if ($exitCode -eq 0) {
Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green 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 { } else {
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red
} }
+106 -15
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-dump v1.1 — Dump external data processor or report (EPF/ERF) to XML sources # epf-dump v1.8 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,84 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def dir_nonempty(path):
"""Postcondition: the platform must have written files into the output directory.
Exit code 0 with an empty dir (broken/headless env) is a false success reject it."""
return os.path.isdir(path) and any(os.scandir(path))
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -90,12 +139,20 @@ def main():
# --- Resolve V8Path --- # --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate database connection --- # --- Validate database connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr) print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.") print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
sys.exit(1) sys.exit(1)
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
if args.Format == "Plain":
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
# --- Validate input file --- # --- Validate input file ---
if not os.path.isfile(args.InputFile): if not os.path.isfile(args.InputFile):
@@ -111,6 +168,34 @@ def main():
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
try: try:
if engine == "ibcmd":
# --- ibcmd branch: dump EPF/ERF via config export --file ---
arguments = ["infobase", "config", "export", f"--file={args.InputFile}", args.OutputDir, f"--db-path={args.InfoBasePath}"]
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
exit_code = result.returncode
out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"External data processor/report dumped successfully to: {args.OutputDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
else:
print(f"Error dumping external data processor/report (code: {exit_code})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(exit_code)
# --- Build arguments --- # --- Build arguments ---
arguments = ["DESIGNER"] arguments = ["DESIGNER"]
@@ -133,7 +218,7 @@ def main():
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
# --- Execute --- # --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}") print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
result = subprocess.run( result = subprocess.run(
[v8path] + arguments, [v8path] + arguments,
capture_output=True, capture_output=True,
@@ -142,8 +227,14 @@ def main():
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- Result ---
# Postcondition: exit 0 with an empty output directory is a false success.
out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir)
if out_missing:
exit_code = 1
if exit_code == 0: if exit_code == 0:
print(f"Dump completed successfully to: {args.OutputDir}") print(f"Dump completed successfully to: {args.OutputDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
else: else:
print(f"Error dumping (code: {exit_code})", file=sys.stderr) print(f"Error dumping (code: {exit_code})", file=sys.stderr)
+1 -1
View File
@@ -34,7 +34,7 @@ allowed-tools:
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. 6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
+1 -1
View File
@@ -33,7 +33,7 @@ allowed-tools:
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. 6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
+25 -4
View File
@@ -1,4 +1,4 @@
# form-add v1.7 — Add managed form to 1C config object # form-add v1.11 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -33,6 +33,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null 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) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -69,10 +79,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} 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 $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { 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 $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -195,7 +208,7 @@ $supportedTypes = @(
"Document", "Catalog", "DataProcessor", "Report", "Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport", "ExternalDataProcessor", "ExternalReport",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes", "InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task" "ExchangePlan", "BusinessProcess", "Task", "DocumentJournal"
) )
$objectType = $null $objectType = $null
@@ -471,7 +484,10 @@ if (-not $childObjects) {
exit 1 exit 1
} }
# Добавить <Form>$FormName</Form> # Добавить <Form>$FormName</Form> — идемпотентно (не дублировать уже зарегистрированную)
$alreadyRegistered = [bool]$childObjects.SelectSingleNode("md:Form[text()='$FormName']", $nsMgr)
if (-not $alreadyRegistered) {
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") $formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formElem.InnerText = $FormName $formElem.InnerText = $FormName
@@ -525,6 +541,7 @@ if ($insertBefore) {
} }
} }
} }
}
# --- SetDefault --- # --- SetDefault ---
@@ -590,7 +607,11 @@ Write-Host " Metadata: $objDirName\$objBaseName\Forms\$FormName.xml"
Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml" Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml"
Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl" Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
Write-Host "" Write-Host ""
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects" if ($alreadyRegistered) {
Write-Host "Already registered: <Form>$FormName</Form> in ChildObjects (skipped duplicate)"
} else {
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
}
if ($defaultUpdated) { if ($defaultUpdated) {
Write-Host "${defaultPropName}: $defaultValue" Write-Host "${defaultPropName}: $defaultValue"
} }
+103 -44
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-add v1.7 — Add managed form to 1C config object # form-add v1.11 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -32,6 +32,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -71,6 +83,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) 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) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -78,6 +93,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
@@ -193,14 +210,50 @@ def detect_format_version(d):
return "2.17" return "2.17"
def save_xml_with_bom(tree, path): def _detect_xml_style(path):
"""Save XML tree to file with UTF-8 BOM.""" """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") финальный перенос. None файл новый (сохранить текущее поведение)."""
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>') try:
if not xml_bytes.endswith(b"\n"): raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_with_bom(tree, path):
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf") if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes) f.write(xml_bytes)
@@ -256,7 +309,7 @@ def main():
"Document", "Catalog", "DataProcessor", "Report", "Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport", "ExternalDataProcessor", "ExternalReport",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes", "InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task", "ExchangePlan", "BusinessProcess", "Task", "DocumentJournal",
] ]
object_type = None object_type = None
@@ -539,47 +592,50 @@ def main():
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr) print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# Add <Form>$FormName</Form> # Add <Form>$FormName</Form> — idempotent (do not duplicate already-registered form)
form_elem = etree.Element(f"{{{ns}}}Form") already_registered = child_objects.find(f"md:Form[.='{form_name}']", NSMAP) is not None
form_elem.text = form_name
# Find first <Template> to insert before it if not already_registered:
first_template = child_objects.find("md:Template", NSMAP) form_elem = etree.Element(f"{{{ns}}}Form")
# Find first <TabularSection> to insert before it (if no Template) form_elem.text = form_name
first_tabular = child_objects.find("md:TabularSection", NSMAP)
# Determine insertion point: before Template, before TabularSection, or at end # Find first <Template> to insert before it
insert_before = None first_template = child_objects.find("md:Template", NSMAP)
if first_template is not None: # Find first <TabularSection> to insert before it (if no Template)
insert_before = first_template first_tabular = child_objects.find("md:TabularSection", NSMAP)
elif first_tabular is not None:
insert_before = first_tabular
if insert_before is not None: # Determine insertion point: before Template, before TabularSection, or at end
# Insert before the found element insert_before = None
idx = list(child_objects).index(insert_before) if first_template is not None:
child_objects.insert(idx, form_elem) insert_before = first_template
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before) elif first_tabular is not None:
form_elem.tail = "\n\t\t\t" insert_before = first_tabular
else:
# Add to end of ChildObjects if insert_before is not None:
children = list(child_objects) # Insert before the found element
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""): idx = list(child_objects).index(insert_before)
# Empty ChildObjects (self-closing) child_objects.insert(idx, form_elem)
child_objects.text = "\n\t\t\t" # Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
child_objects.append(form_elem) form_elem.tail = "\n\t\t\t"
form_elem.tail = "\n\t\t"
else: else:
if len(children) > 0: # Add to end of ChildObjects
last_child = children[-1] children = list(child_objects)
old_tail = last_child.tail if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
last_child.tail = "\n\t\t\t" # Empty ChildObjects (self-closing)
child_objects.append(form_elem) child_objects.text = "\n\t\t\t"
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem) child_objects.append(form_elem)
form_elem.tail = "\n\t\t" form_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# --- SetDefault --- # --- SetDefault ---
@@ -624,7 +680,10 @@ def main():
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml") print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl") print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
print() print()
print(f"Registered: <Form>{form_name}</Form> in ChildObjects") if already_registered:
print(f"Already registered: <Form>{form_name}</Form> in ChildObjects (skipped duplicate)")
else:
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
if default_updated: if default_updated:
print(f"{default_prop_name}: {default_value}") print(f"{default_prop_name}: {default_value}")
print() print()
+1
View File
@@ -187,6 +187,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
| `showTitle: true` | Показывать заголовок группы | | `showTitle: true` | Показывать заголовок группы |
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) | | `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой | | `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
| `controlRepresentation` | Отображение управления свёрткой (`behavior: "collapsible"`): `"TitleHyperlink"` (гиперссылка заголовка, по умолчанию) / `"Picture"` (картинка) |
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` | | `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
| `children: [...]` | Вложенные элементы | | `children: [...]` | Вложенные элементы |
@@ -1,4 +1,4 @@
# form-compile v1.174 — Compile 1C managed form from JSON or object metadata # form-compile v1.175 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[string]$JsonPath, [string]$JsonPath,
@@ -1362,6 +1362,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null 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) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -1398,10 +1408,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} 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 $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { 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 $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-compile v1.174 — Compile 1C managed form from JSON or object metadata # form-compile v1.175 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import copy import copy
@@ -34,6 +34,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -73,6 +85,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) 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) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -80,6 +95,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
+14 -1
View File
@@ -1,4 +1,4 @@
# form-edit v1.3 — Edit 1C managed form elements # form-edit v1.5 — Edit 1C managed form elements
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -27,6 +27,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null 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) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -63,10 +73,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} 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 $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { 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 $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
+49 -6
View File
@@ -1,4 +1,4 @@
# form-edit v1.3 — Edit 1C managed form elements (Python port) # form-edit v1.5 — Edit 1C managed form elements (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import json import json
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) 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) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
@@ -1458,14 +1475,40 @@ if elem_events_list:
# ── 13. Save ──────────────────────────────────────────────── # ── 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") xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
# Fix XML declaration quotes # Восстановить регистр encoding как в оригинале.
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>') xml_bytes = xml_bytes.replace(
if not xml_bytes.endswith(b"\n"): 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" 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: 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) f.write(xml_bytes)
# ── 14. Summary ───────────────────────────────────────────── # ── 14. Summary ─────────────────────────────────────────────
+15 -2
View File
@@ -1,4 +1,4 @@
# form-info v1.4 — Analyze 1C managed form structure # form-info v1.5 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory=$true)] [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 # 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 # uuid of the nearest element meta-xml (form/template/etc.) and the config root
# bin. Never throws — degrades to "не на поддержке". # 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) { function Get-SupportStatusForPath([string]$targetPath) {
try { try {
$rp = (Resolve-Path $targetPath).Path $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). # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
$elemUuid = Get-RootUuid $rp $elemUuid = Get-RootUuid $rp
if (Test-ExternalObjectRoot $rp) { return $null }
$d = [System.IO.Path]::GetDirectoryName($rp) $d = [System.IO.Path]::GetDirectoryName($rp)
for ($i = 0; $i -lt 12 -and $d; $i++) { 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 $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $binPath) { if (-not $binPath) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -448,7 +460,8 @@ if ($formTitle) { $header += " — `"$formTitle`"" }
if ($objectContext) { $header += " ($objectContext)" } if ($objectContext) { $header += " ($objectContext)" }
$header += " ===" $header += " ==="
$lines += $header $lines += $header
$lines += "Поддержка: $(Get-SupportStatusForPath $FormPath)" $support = Get-SupportStatusForPath $FormPath
if ($null -ne $support) { $lines += "Поддержка: $support" }
# --- Form properties (Title excluded — shown in header) --- # --- Form properties (Title excluded — shown in header) ---
+19 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-info v1.4 — Analyze 1C managed form structure # form-info v1.5 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -353,14 +353,29 @@ def get_support_status_for_path(target_path):
except Exception: except Exception:
pass pass
return None return None
def is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
rp = os.path.abspath(target_path) rp = os.path.abspath(target_path)
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
elem_uuid = root_uuid(rp) elem_uuid = root_uuid(rp)
if is_external_root(rp):
return None
bin_path = None bin_path = None
d = os.path.dirname(rp) d = os.path.dirname(rp)
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if is_external_root(d + ".xml"):
return None
if not elem_uuid: if not elem_uuid:
elem_uuid = root_uuid(d + ".xml") elem_uuid = root_uuid(d + ".xml")
if not bin_path: if not bin_path:
@@ -513,7 +528,9 @@ def main():
header += f" ({object_context})" header += f" ({object_context})"
header += " ===" header += " ==="
lines.append(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) --- # --- Form properties (Title excluded -- shown in header) ---
prop_names = [ prop_names = [
@@ -1,4 +1,4 @@
# form-remove v1.2 — Remove form from 1C object # form-remove v1.4 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -68,10 +68,14 @@ foreach ($node in $formNodes) {
} }
} }
# Очистить DefaultForm если указывала на эту форму # Очистить любые Default*/Auxiliary* form-слоты, указывавшие на удалённую форму
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr) # (form-add пишет свойство по назначению: DefaultObjectForm/DefaultListForm/
if ($defaultForm -and $defaultForm.InnerText -match "Form\.$FormName$") { # DefaultChoiceForm/DefaultRecordForm/DefaultForm — не только generic DefaultForm).
$defaultForm.InnerText = "" $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 = ""
}
} }
# Сохранить с BOM # Сохранить с BOM
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# remove-form v1.1 — Remove form from 1C object # remove-form v1.4 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -13,14 +13,50 @@ from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"} NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path): def _detect_xml_style(path):
"""Save XML tree to file with UTF-8 BOM.""" """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") финальный перенос. None файл новый (сохранить текущее поведение)."""
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>') try:
if not xml_bytes.endswith(b"\n"): raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_with_bom(tree, path):
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf") if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes) f.write(xml_bytes)
@@ -85,11 +121,15 @@ def main():
parent.remove(node) parent.remove(node)
break break
# Clear DefaultForm if it pointed to removed form # Clear any Default*/Auxiliary* form slot that pointed to the removed form
default_form = root.find(".//md:DefaultForm", NSMAP) # (form-add writes the purpose-specific property: DefaultObjectForm / DefaultListForm /
if default_form is not None and default_form.text: # DefaultChoiceForm / DefaultRecordForm / DefaultForm — not just generic DefaultForm).
if re.search(rf"Form\.{re.escape(form_name)}$", default_form.text): ref_re = re.compile(rf"Form\.{re.escape(form_name)}$")
default_form.text = "" for el in root.iter():
if not isinstance(el.tag, str):
continue
if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text):
el.text = ""
# Save with BOM # Save with BOM
save_xml_with_bom(tree, root_xml_full) save_xml_with_bom(tree, root_xml_full)
@@ -1,4 +1,4 @@
# form-validate v1.7 — Validate 1C managed form # form-validate v1.8 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -382,6 +382,10 @@ if (-not $stopped) {
$pathChecked = 0 $pathChecked = 0
$pathBaseSkipped = 0 $pathBaseSkipped = 0
# All data-binding tags whose value is an attribute path (root must exist in <Attributes>).
$bindingTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath',
'MultipleValueDataPath','MultipleValuePresentDataPath','RowPictureDataPath','MultipleValuePictureDataPath')
foreach ($el in $allElements) { foreach ($el in $allElements) {
if ($stopped) { break } if ($stopped) { break }
$tag = $el.Tag $tag = $el.Tag
@@ -398,60 +402,63 @@ if (-not $stopped) {
try { if ([int]$el.Id -lt 1000000) { $pathBaseSkipped++; continue } } catch {} try { if ([int]$el.Id -lt 1000000) { $pathBaseSkipped++; continue } } catch {}
} }
$dpNode = $node.SelectSingleNode("f:DataPath", $nsMgr) foreach ($bTag in $bindingTags) {
if (-not $dpNode) { continue } if ($stopped) { break }
$dpNode = $node.SelectSingleNode("f:$bTag", $nsMgr)
if (-not $dpNode) { continue }
$dataPath = $dpNode.InnerText.Trim() $dataPath = $dpNode.InnerText.Trim()
if (-not $dataPath) { continue } if (-not $dataPath) { continue }
# Opaque platform-internal DataPath shapes — not validatable from Form.xml alone: # Opaque platform-internal shapes — not validatable from Form.xml alone:
# - bare numeric (e.g. "10", "1000003") — internal index # - bare numeric (e.g. "10", "1000003") — internal index
# - "N/M:<uuid>" — metadata reference by UUID # - "N/M:<uuid>" — metadata reference by UUID
if ($dataPath -match '^\d+$' -or $dataPath -match '^\d+/\d+:[0-9a-fA-F-]+$') { if ($dataPath -match '^\d+$' -or $dataPath -match '^\d+/\d+:[0-9a-fA-F-]+$') {
continue
}
$pathChecked++
# Extract root segment of path, strip array indices like [0]
$cleanPath = $dataPath -replace '\[\d+\]', ''
# Strip leading '~' (current row of DynamicList: ~Список.Поле)
if ($cleanPath.StartsWith('~')) { $cleanPath = $cleanPath.Substring(1) }
$segments = $cleanPath -split '\.'
$rootAttr = $segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
if ($rootAttr -eq 'Items') {
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
Report-Warn "[$tag] '$elName': DataPath='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
continue continue
} }
$tableName = $segments[1]
$tableEl = $null $pathChecked++
foreach ($candidate in $allElements) {
if ($candidate.Tag -eq 'Table' -and $candidate.Name -eq $tableName) { # Extract root segment of path, strip array indices like [0]
$tableEl = $candidate $cleanPath = $dataPath -replace '\[\d+\]', ''
break # Strip leading '~' (current row of DynamicList: ~Список.Поле)
if ($cleanPath.StartsWith('~')) { $cleanPath = $cleanPath.Substring(1) }
$segments = $cleanPath -split '\.'
$rootAttr = $segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
if ($rootAttr -eq 'Items') {
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
Report-Warn "[$tag] '$elName': $bTag='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
continue
} }
$tableName = $segments[1]
$tableEl = $null
foreach ($candidate in $allElements) {
if ($candidate.Tag -eq 'Table' -and $candidate.Name -eq $tableName) {
$tableEl = $candidate
break
}
}
if (-not $tableEl) {
Report-Error "[$tag] '$elName': $bTag='$dataPath' — table element '$tableName' not found"
$pathErrors++
continue
}
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
# Table without DataPath — can't resolve further, accept silently
continue
}
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
$rootAttr = ($tableDp -split '\.')[0]
} }
if (-not $tableEl) {
Report-Error "[$tag] '$elName': DataPath='$dataPath' — table element '$tableName' not found"
$pathErrors++
continue
}
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
# Table without DataPath — can't resolve further, accept silently
continue
}
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
$rootAttr = ($tableDp -split '\.')[0]
}
if (-not $attrMap.ContainsKey($rootAttr)) { if (-not $attrMap.ContainsKey($rootAttr)) {
Report-Error "[$tag] '$elName': DataPath='$dataPath' — attribute '$rootAttr' not found" Report-Error "[$tag] '$elName': $bTag='$dataPath' — attribute '$rootAttr' not found"
$pathErrors++ $pathErrors++
}
} }
} }
@@ -462,9 +469,9 @@ if (-not $stopped) {
$pathMsg = if ($pathMsg) { "$pathMsg, $skipNote" } else { $skipNote } $pathMsg = if ($pathMsg) { "$pathMsg, $skipNote" } else { $skipNote }
} }
if ($pathErrors -eq 0 -and $pathMsg) { if ($pathErrors -eq 0 -and $pathMsg) {
Report-OK "DataPath references: $pathMsg" Report-OK "Data bindings: $pathMsg"
} elseif ($pathErrors -eq 0) { } elseif ($pathErrors -eq 0) {
Report-OK "DataPath references: none" Report-OK "Data bindings: none"
} }
} }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-validate v1.7 — Validate 1C managed form # form-validate v1.8 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -379,6 +379,10 @@ def main():
path_checked = 0 path_checked = 0
path_base_skipped = 0 path_base_skipped = 0
# All data-binding tags whose value is an attribute path (root must exist in <Attributes>).
binding_tags = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath",
"MultipleValueDataPath", "MultipleValuePresentDataPath", "RowPictureDataPath", "MultipleValuePictureDataPath"]
skip_tags = {"ContextMenu", "ExtendedTooltip", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition"} skip_tags = {"ContextMenu", "ExtendedTooltip", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition"}
for el in all_elements: for el in all_elements:
@@ -399,55 +403,58 @@ def main():
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
dp_node = node.find(f"{{{F_NS}}}DataPath") for b_tag in binding_tags:
if dp_node is None: if stopped:
continue break
dp_node = node.find(f"{{{F_NS}}}{b_tag}")
data_path = (dp_node.text or "").strip() if dp_node is None:
if not data_path:
continue
# Opaque platform-internal DataPath shapes — not validatable from Form.xml alone:
# - bare numeric (e.g. "10", "1000003") — internal index
# - "N/M:<uuid>" — metadata reference by UUID
if re.match(r'^\d+$', data_path) or re.match(r'^\d+/\d+:[0-9a-fA-F-]+$', data_path):
continue
path_checked += 1
clean_path = re.sub(r'\[\d+\]', '', data_path)
# Strip leading '~' (current row of DynamicList: ~\u0421\u043f\u0438\u0441\u043e\u043a.\u041f\u043e\u043b\u0435)
if clean_path.startswith('~'):
clean_path = clean_path[1:]
segments = clean_path.split(".")
root_attr = segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... \u2014 table element, not attribute
if root_attr == 'Items':
if len(segments) < 3 or segments[2] != 'CurrentData':
report_warn(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 unknown Items.* shape, expected Items.<Table>.CurrentData.*")
continue continue
table_name = segments[1]
table_el = None data_path = (dp_node.text or "").strip()
for candidate in all_elements: if not data_path:
if candidate["Tag"] == 'Table' and candidate["Name"] == table_name: continue
table_el = candidate
break # Opaque platform-internal shapes — not validatable from Form.xml alone:
if table_el is None: # - bare numeric (e.g. "10", "1000003") — internal index
report_error(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 table element '{table_name}' not found") # - "N/M:<uuid>" — metadata reference by UUID
if re.match(r'^\d+$', data_path) or re.match(r'^\d+/\d+:[0-9a-fA-F-]+$', data_path):
continue
path_checked += 1
clean_path = re.sub(r'\[\d+\]', '', data_path)
# Strip leading '~' (current row of DynamicList: ~Список.Поле)
if clean_path.startswith('~'):
clean_path = clean_path[1:]
segments = clean_path.split(".")
root_attr = segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
if root_attr == 'Items':
if len(segments) < 3 or segments[2] != 'CurrentData':
report_warn(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — unknown Items.* shape, expected Items.<Table>.CurrentData.*")
continue
table_name = segments[1]
table_el = None
for candidate in all_elements:
if candidate["Tag"] == 'Table' and candidate["Name"] == table_name:
table_el = candidate
break
if table_el is None:
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — table element '{table_name}' not found")
path_errors += 1
continue
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
if table_dp_node is None or not (table_dp_node.text or "").strip():
continue
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
if table_dp.startswith('~'):
table_dp = table_dp[1:]
root_attr = table_dp.split(".")[0]
if root_attr not in attr_map:
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — attribute '{root_attr}' not found")
path_errors += 1 path_errors += 1
continue
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
if table_dp_node is None or not (table_dp_node.text or "").strip():
continue
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
if table_dp.startswith('~'):
table_dp = table_dp[1:]
root_attr = table_dp.split(".")[0]
if root_attr not in attr_map:
report_error(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 attribute '{root_attr}' not found")
path_errors += 1
path_msg = "" path_msg = ""
if path_checked > 0: if path_checked > 0:
@@ -456,7 +463,7 @@ def main():
skip_note = f"{path_base_skipped} base skipped" skip_note = f"{path_base_skipped} base skipped"
path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note
if path_errors == 0 and path_msg: if path_errors == 0 and path_msg:
report_ok(f"DataPath references: {path_msg}") report_ok(f"Data bindings: {path_msg}")
# --- Check 6: Button command references --- # --- Check 6: Button command references ---
if not stopped: if not stopped:
@@ -724,7 +731,7 @@ def main():
# ExternalDataProcessorObject/ExternalReportObject valid only in EPF/ERF context # ExternalDataProcessorObject/ExternalReportObject valid only in EPF/ERF context
if is_config_context and prefix in ('ExternalDataProcessorObject', 'ExternalReportObject'): if is_config_context and prefix in ('ExternalDataProcessorObject', 'ExternalReportObject'):
report_error(f'12. Type "{tv}": External* type in configuration context (use DataProcessorObject/ReportObject instead)') report_error(f'12. Type "{tv}": External* type in configuration context (use DataProcessorObject/ReportObject instead)')
type_invalid += 1 type_error_count += 1
else: else:
report_warn(f'12. Type "{tv}": unrecognized cfg prefix') report_warn(f'12. Type "{tv}": unrecognized cfg prefix')
type_warn_count += 1 type_warn_count += 1
+14 -1
View File
@@ -1,4 +1,4 @@
# help-add v1.7 — Add built-in help to 1C object # help-add v1.9 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -28,6 +28,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null 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) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -64,10 +74,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} 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 $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { 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 $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
+60 -7
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# add-help v1.7 — Add built-in help to 1C object # add-help v1.9 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -33,6 +33,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -72,6 +84,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) 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) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -79,6 +94,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
@@ -188,14 +205,50 @@ def detect_format_version(d):
return "2.17" return "2.17"
def save_xml_with_bom(tree, path): def _detect_xml_style(path):
"""Save XML tree to file with UTF-8 BOM.""" """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") финальный перенос. None файл новый (сохранить текущее поведение)."""
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>') try:
if not xml_bytes.endswith(b"\n"): raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_with_bom(tree, path):
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf") if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes) f.write(xml_bytes)
@@ -1,3 +1,6 @@
#!/usr/bin/env python3
# img-grid v1.1 — 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. """Overlay a numbered grid on an image to help determine column/row proportions.
Usage: python overlay-grid.py <image> [-c COLS] [-r ROWS] [-o OUTPUT] Usage: python overlay-grid.py <image> [-c COLS] [-r ROWS] [-o OUTPUT]
@@ -29,6 +32,11 @@ def main():
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)") parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
args = parser.parse_args() args = parser.parse_args()
if args.cols <= 0:
parser.error("--cols must be greater than 0")
if args.rows < 0:
parser.error("--rows must be greater than or equal to 0")
src = Image.open(args.image).convert("RGBA") src = Image.open(args.image).convert("RGBA")
sw, sh = src.size sw, sh = src.size
@@ -36,7 +44,7 @@ def main():
step_x = sw / cols step_x = sw / cols
rows = args.rows rows = args.rows
if rows == 0: if rows == 0:
rows = round(sh / step_x) rows = max(1, round(sh / step_x))
step_y = sh / rows step_y = sh / rows
# Canvas with margins for labels # Canvas with margins for labels
@@ -1,4 +1,4 @@
# interface-edit v1.6 — Edit 1C CommandInterface.xml # interface-edit v1.8 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)][Alias('Path')][string]$CIPath, [Parameter(Mandatory)][Alias('Path')][string]$CIPath,
@@ -39,6 +39,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null 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) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -75,10 +85,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} 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 $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { 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 $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# interface-edit v1.6 — Edit 1C CommandInterface.xml # interface-edit v1.8 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) 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) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
@@ -270,13 +287,49 @@ def parse_value_list(val):
return [val] return [val]
def save_xml_bom(tree, path): def _detect_xml_style(path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>') финальный перенос. None файл новый (сохранить текущее поведение)."""
if not xml_bytes.endswith(b"\n"): try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_bom(tree, path):
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf") if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes) f.write(xml_bytes)
+80 -61
View File
@@ -9,18 +9,19 @@ allowed-tools:
- Glob - Glob
--- ---
# /meta-compile — генерация объектов метаданных из JSON DSL # /meta-compile — генерация объектов метаданных из JSON
Принимает JSON-определение объекта метаданных → генерирует XML + модули в структуре выгрузки конфигурации + регистрирует в Configuration.xml. Принимает JSON-определение объекта → генерирует XML + модули в структуре выгрузки конфигурации и
регистрирует объект в `Configuration.xml`.
`ConfigDumpInfo.xml` намеренно не трогается: это служебный файл версий объектов, которым управляет
платформа (для инкрементальной выгрузки).
## Порядок работы ## Порядок работы
1. Составь JSON по синтаксису и примерам ниже → запиши во временный файл 1. Составь JSON по синтаксису ниже → запиши во временный файл.
2. Запусти скрипт meta-compile 2. Запусти скрипт.
3. Если нужно изменить созданный объект — `/meta-edit` 3. Изменить созданный объект — `/meta-edit`; проверить — `/meta-validate`.
4. Если нужно проверить — `/meta-validate`
## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>" 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-файлу (один объект `{...}` или массив `[{...}, ...]`) | | `JsonPath` | Путь к JSON-файлу |
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/` и т.д.) | | `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/`, …) |
## JSON DSL ## Формат JSON
### Общая структура **Один объект** `{ ... }` или **массив** объектов `[{ ... }, { ... }]` (batch — несколько объектов за прогон).
```json ```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) по умолчанию "Имя" → String(10)
"ИмяРеквизита: Тип" → с типом "Имя: Тип" → с типом
"ИмяРеквизита: Тип | req, index" → с флагами "Имя: Тип | 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 ```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 ```json
{ { "type": "Catalog", "name": "Организации", "descriptionLength": 100,
"type": "Catalog", "name": "Организации", "attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"] }
"descriptionLength": 100,
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"]
}
``` ```
### С табличной частью Документ с движениями и ТЧ:
```json ```json
{ { "type": "Document", "name": "ПриходнаяНакладная",
"type": "Document", "name": "ПриходнаяНакладная",
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"], "registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"], "attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] } "tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] } }
}
``` ```
### Регистровый паттерн (измерения + ресурсы) Регистр сведений:
```json ```json
{ { "type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"], "dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] "resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] }
}
``` ```
### Batch — несколько объектов в одном файле Batch:
```json ```json
[ [ { "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
{ "type": "Catalog", "name": "Валюты" }, { "type": "Catalog", "name": "Валюты" },
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" } { "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" } ]
]
``` ```
@@ -0,0 +1,139 @@
# Объектная форма реквизита и табличной части
Когда реквизиту (в `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` | — | кастомизация стандартного реквизита НомерСтроки (см. ниже) |
### `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` | `[]` | массив ссылок-владельцев: `["CatalogRef.Контрагенты"]` |
| `subordinationUse` | `ToItems` | `ToItems` / `ToFolders` / `ToFoldersAndItems` (кому подчинён) |
| `codeLength` | `9` | длина кода (0 — без кода) |
| `codeType` | `String` | `String` / `Number` |
| `codeAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `codeSeries` | `WholeCatalog` | `WholeCatalog` / `WithinSubordination` / `WithinOwnerSubordination` |
| `autonumbering` | `true` | bool (автонумерация) |
| `checkUnique` | `false` | bool (контроль уникальности кода) |
| `descriptionLength` | `25` | длина наименования |
| `defaultPresentation` | `AsDescription` | `AsDescription` / `AsCode` |
| `quickChoice` | `true` | bool (быстрый выбор) |
| `choiceMode` | `BothWays` | `BothWays` / `QuickChoice` / `FromForm` |
| `editType` | `InDialog` | `InDialog` / `InList` / `BothWays` |
| `createOnInput` | `Use` | `Auto` / `Use` / `DontUse` |
| `choiceHistoryOnInput` | `Auto` | `Auto` / `DontUse` |
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
| `fullTextSearchOnInputByString` | `DontUse` | `Use` / `DontUse` |
| `searchStringModeOnInputByString` | `Begin` | `Begin` / `AnyPart` |
| `predefinedDataUpdate` | `Auto` | `Auto` / `DontAutoUpdate` / `AutoUpdate` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `useStandardCommands` | `true` | bool |
| `includeHelpInContents` | `false` | bool |
| `attributes` | `[]` | реквизиты (shorthand / объектная форма) |
| `tabularSections` | `{}` | табличные части |
**Формы.** Ссылка на форму — `Тип.Объект.Form.ИмяФормы` (напр. `Catalog.Организации.Form.ФормаЭлемента`).
Слоты основных форм: `defaultObjectForm`, `defaultFolderForm`, `defaultListForm`, `defaultChoiceForm`,
`defaultFolderChoiceForm`; вспомогательных — те же имена с префиксом `auxiliary` (`auxiliaryObjectForm`, …).
## `predefined` — предопределённые элементы
Массив предопределённых элементов → `Ext/Predefined.xml`. Элемент — строка (плоский случай) или объект (иерархия).
**Строка:** `"(Код) Имя [Наименование]"``Имя` обязательно; `(Код)` и `[Наименование]` опциональны.
Без `[...]` наименование выводится из имени; `[]` — пустое; `[текст]` — заданное.
```json
"predefined": [
"Основной",
"(1) ДокументОПриемке [Документ о приемке]",
{ "name": "Группа1", "isFolder": true, "description": "Прочие",
"childItems": ["Факс", "(7) Скайп"] }
]
```
**Объект:** `name` (обязательно), `code`, `description` (наименование), `isFolder` (признак группы),
`childItems` (вложенные, рекурсивно). Тип кода — по свойству `codeType`.
## Дополнительно
- Свойства реквизитов и табличных частей — `attributes.md`.
- Представления (`objectPresentation`, `listPresentation`, …), команды объекта, характеристики
(«ДопРеквизиты и сведения»), кастомизация стандартных реквизитов, `inputByString` / `dataLockFields` /
`basedOn``blocks.md`.
@@ -0,0 +1,105 @@
# Планы: ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes
Все три — ссылочные типы (наследуют слой Catalog: коды, `standardAttributes`, `characteristics`, `inputByString`,
формы, представления — см. `catalog.md` / `attributes.md` / `blocks.md`) с предопределёнными элементами и своими
специальными свойствами.
## ChartOfCharacteristicTypes (План видов характеристик)
Хранит определения характеристик (видов). Иерархический (папки+элементы).
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `valueType` | любой примитив | тип значения характеристики (составной — строка `"A + B"` или массив `valueTypes`) |
| `characteristicExtValues` | пусто | ссылка на справочник доп. значений |
| `hierarchical` | `false` | bool |
| `foldersOnTop` | `true` | bool |
| `codeLength` | `9` | длина кода |
| `descriptionLength` | `100` | длина наименования |
| `checkUnique` | `true` | bool |
| `autonumbering` | `true` | bool |
| `codeSeries` | `WholeCharacteristicKind` | серия кодов |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `predefined` | `[]` | предопределённые виды (несут тип значения — см. ниже) |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
**Предопределённые виды** несут **тип значения на элемент** — короткой строкой после `:`
(`"(Код) Имя [Наименование]: Тип"`, составной через `+`) или объектной формой с ключом `type`:
```json
"predefined": [
"(000001) Цвет: CatalogRef.Цвета",
"(000002) Размер [Размер одежды]: String(50) + Number(3,0)",
{ "name": "Группа", "isFolder": true, "type": "" }
]
```
## ChartOfAccounts (План счетов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `extDimensionTypes` | пусто | ссылка на ПВХ видов субконто `ChartOfCharacteristicTypes.X` |
| `maxExtDimensionCount` | `0` (без ПВХ) / `3` (с ПВХ) | макс. число субконто |
| `codeMask` | пусто | маска кода счёта (напр. `"@@@.@@"`) |
| `codeLength` | `9` | длина кода |
| `descriptionLength` | `25` | длина наименования |
| `checkUnique` | `true` | bool |
| `codeSeries` | `WholeChartOfAccounts` | серия кодов |
| `defaultPresentation` | `AsCode` | `AsCode` / `AsDescription` |
| `autoOrderByCode` | `true` | bool |
| `orderLength` | `9` | длина строки упорядочивания |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `accountingFlags` | `[]` | признаки учёта (как реквизиты, тип по умолчанию Boolean; массив имён/реквизитов) |
| `extDimensionAccountingFlags` | `[]` | признаки учёта субконто (как реквизиты) |
| `predefined` | `[]` | предопределённые счета (см. ниже) |
**Предопределённый счёт** (объектная форма):
| Поле | Умолчание | Значения |
|------|-----------|----------|
| `name` | — | имя (обязательно) |
| `code` | пусто | код счёта |
| `description` | из имени | наименование |
| `accountType` | `ActivePassive` | `Active` / `Passive` / `ActivePassive` |
| `offBalance` | `false` | bool (забалансовый) |
| `order` | — | строка сортировки |
| `flags` | `[]` | включённые признаки учёта (только TRUE) |
| `subconto` | `[]` | виды субконто (см. ниже) |
| `childItems` | `[]` | подчинённые счета |
`subconto` — строка `"Вид | Признак1, Признак2"` (после `|` — включённые признаки учёта субконто; токен `Turnover`
«только обороты») или объект `{ type, turnover, flags }`. `Вид` — имя предопределённого вида из ПВХ `extDimensionTypes`.
```json
"predefined": [
{ "name": "ОсновныеСредства", "code": "01", "accountType": "Active", "order": " 01",
"flags": ["Количественный"], "subconto": ["Номенклатура | Суммовой, Валютный"],
"childItems": [ { "name": "ОСВОрганизации", "code": "01.01", "accountType": "Active", "order": " 01.01" } ] }
]
```
## ChartOfCalculationTypes (План видов расчёта)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `codeLength` | `5` | длина кода |
| `descriptionLength` | `100` | длина наименования |
| `codeAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `dependenceOnCalculationTypes` | `DontUse` | `DontUse` / `OnPeriod` / `OnActionPeriod` |
| `baseCalculationTypes` | `[]` | базовые виды расчёта (список ссылок `ChartOfCalculationTypes.X`) |
| `actionPeriodUse` | `false` | bool (использовать период действия) |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `predefined` | `[]` | предопределённые виды расчёта (см. ниже) |
**Предопределённый вид расчёта** — плоский: строка `"(Код) Имя [Наименование]"` или объект
`{ name, code, description, actionPeriodIsBase }` (`actionPeriodIsBase` — bool, по умолчанию `false`).
```json
"predefined": [ "(00001) Оклад [Оклад по дням]", { "name": "Премия", "code": "00002", "actionPeriodIsBase": true } ]
```
> **ChartOfAccounts** ссылается на ПВХ через `extDimensionTypes`. Регистр бухгалтерии/расчёта требует
> соответствующий план (см. `registers.md`).
@@ -0,0 +1,56 @@
# CommonModule, ScheduledJob, EventSubscription (объекты, привязанные к коду)
## CommonModule (Общий модуль)
Флаги контекста выполнения (все bool, по умолчанию `false`). Создаёт пустой `Ext/Module.bsl`.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `context` | — | шорткат флагов (см. ниже) |
| `global` | `false` | bool |
| `server` | `false` | bool |
| `serverCall` | `false` | bool (вызов сервера) |
| `clientManagedApplication` | `false` | bool (клиент управляемого приложения) |
| `clientOrdinaryApplication` | `false` | bool (клиент обычного приложения) |
| `externalConnection` | `false` | bool |
| `privileged` | `false` | bool |
| `returnValuesReuse` | `DontUse` | `DontUse` / `DuringRequest` / `DuringSession` |
Шорткат `context`: `"server"` → Server+ServerCall; `"client"` → ClientManagedApplication;
`"serverClient"` → Server+ClientManagedApplication.
```json
{ "type": "CommonModule", "name": "ОбменДаннымиСервер", "context": "server", "returnValuesReuse": "DuringRequest" }
```
## ScheduledJob (Регламентное задание)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `methodName` | пусто | метод-обработчик `"МодульСервер.Процедура"` (дополняется до `CommonModule.…`) |
| `description` | пусто | наименование задания |
| `key` | пусто | ключ |
| `use` | `false` | bool (использование) |
| `predefined` | `false` | bool (предопределённое) |
| `restartCountOnFailure` | `3` | число повторов при сбое |
| `restartIntervalOnFailure` | `10` | интервал повтора, сек |
```json
{ "type": "ScheduledJob", "name": "ОбменДанными", "methodName": "ОбменДаннымиСервер.Выполнить", "use": true }
```
## EventSubscription (Подписка на событие)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `source` | `[]` | объекты-источники: `["CatalogObject.Контрагенты", "DocumentObject.Реализация"]` |
| `event` | `BeforeWrite` | `BeforeWrite` / `OnWrite` / `BeforeDelete` / `OnReadAtServer` / `FillCheckProcessing` … |
| `handler` | пусто | метод-обработчик `"МодульСервер.Процедура"` (дополняется до `CommonModule.…`) |
```json
{ "type": "EventSubscription", "name": "ПередЗаписьюКонтрагента",
"source": ["CatalogObject.Контрагенты"], "event": "BeforeWrite",
"handler": "ОбщегоНазначенияСервер.ПередЗаписьюКонтрагента" }
```
> Процедура-обработчик (`methodName` / `handler`) должна существовать в указанном общем модуле (экспортная).
@@ -0,0 +1,79 @@
# Document, DocumentJournal, Sequence, DocumentNumerator
## Document (Документ)
```json
{ "type": "Document", "name": "ПриходнаяНакладная",
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
"attributes": ["Организация: CatalogRef.Организации"],
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] } }
```
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `numerator` | пусто | ссылка на нумератор `DocumentNumerator.X` |
| `numberType` | `String` | `String` / `Number` |
| `numberLength` | `11` | длина номера |
| `numberAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `numberPeriodicity` | `Year` | `Nonperiodical` / `Day` / `Month` / `Quarter` / `Year` |
| `checkUnique` | `true` | bool |
| `autonumbering` | `true` | bool |
| `posting` | `Allow` | `Allow` / `Deny` (проведение) |
| `realTimePosting` | `Deny` | `Allow` / `Deny` (оперативное проведение) |
| `registerRecordsDeletion` | `AutoDelete` | `AutoDelete` / `AutoDeleteOnUnpost` / `AutoDeleteOff` |
| `registerRecordsWritingOnPost` | `WriteSelected` | `WriteModified` / `WriteSelected` / `WriteAll` |
| `sequenceFilling` | `AutoFill` | заполнение последовательностей |
| `postInPrivilegedMode` | `true` | bool |
| `unpostInPrivilegedMode` | `true` | bool |
| `createOnInput` | `Use` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
| `dataHistory` | `DontUse` | `Use` / `DontUse` |
| `registerRecords` | `[]` | движения: список ссылок `["AccumulationRegister.ОстаткиТоваров", "InformationRegister.Цены"]` |
| `useStandardCommands` | `true` | bool |
| `includeHelpInContents` | `false` | bool |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
Формы: `defaultObjectForm`, `defaultListForm`, `defaultChoiceForm`, `auxiliary*` (см. `catalog.md`).
Реквизиты и ТЧ — `attributes.md`. Представления, команды, характеристики, `basedOn`, `standardAttributes`,
`inputByString`, `dataLockFields``blocks.md`.
## DocumentJournal (Журнал документов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `registeredDocuments` | `[]` | документы журнала: `["Document.Встреча", "Document.Звонок"]` |
| `columns` | `[]` | графы журнала (см. ниже) |
Графа — строка `"Имя"` или объект `{ name, synonym, indexing, references }`, где `indexing``Index`/`DontIndex`,
`references` — пути к реквизитам документов, отображаемым в графе.
```json
{ "type": "DocumentJournal", "name": "Взаимодействия",
"registeredDocuments": ["Document.Встреча", "Document.Звонок"],
"columns": [{ "name": "Организация", "indexing": "Index",
"references": ["Document.Встреча.Attribute.Организация"] }] }
```
## Sequence (Последовательность документов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `moveBoundaryOnPosting` | `DontMove` | сдвиг границы при проведении |
| `documents` | `[]` | документы последовательности (список ссылок) |
| `registerRecords` | `[]` | движения (список ссылок) |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dimensions` | `[]` | измерения `{name, type, documentMap[], registerRecordsMap[]}` |
`documentMap` / `registerRecordsMap` — пути к реквизитам документов / движениям, соответствующим измерению.
## DocumentNumerator (Нумератор документов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `numberType` | `String` | `String` / `Number` |
| `numberLength` | `11` | длина номера |
| `numberAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `numberPeriodicity` | `Year` | `Nonperiodical` / `Day` / … / `Year` |
| `checkUnique` | `true` | bool |
@@ -0,0 +1,41 @@
# ExchangePlan (План обмена)
Близок к справочнику (без иерархии/владельцев), плюс состав объектов обмена. Наследует слой Catalog:
`codeLength`, `codeAllowedLength`, `descriptionLength`, `defaultPresentation`, `editType`, `quickChoice`,
`choiceMode`, формы, `standardAttributes`, `characteristics`, `inputByString`, `basedOn`, представления —
см. `catalog.md` / `attributes.md` / `blocks.md`.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `distributedInfoBase` | `false` | bool (распределённая ИБ — РИБ) |
| `includeConfigurationExtensions` | `false` | bool (включать расширения конфигурации) |
| `descriptionLength` | `150` | длина наименования |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dataHistory` | `DontUse` | `Use` / `DontUse` |
| `useStandardCommands` | `true` | bool |
| `content` | `[]` | состав обмена (см. ниже) |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
## `content` — состав плана обмена
Список объектов-участников обмена; у каждого — признак авторегистрации изменений (по умолчанию выключена).
Элемент — ссылка на объект метаданных (строка) или объект с признаком:
```json
"content": [
"Catalog.Организации", // авторегистрация выключена
"InformationRegister.Курсы: autoRecord", // авторегистрация включена (токен)
{ "metadata": "Document.РеализацияТоваров", "autoRecord": true }
]
```
- Строка `"Тип.Имя"` — авторегистрация выключена; суффикс `: autoRecord` — включена.
- Объект: `metadata` (ссылка), `autoRecord` (bool или `Allow`/`Deny`).
```json
{ "type": "ExchangePlan", "name": "ОбменССайтом", "distributedInfoBase": false,
"content": ["Catalog.Номенклатура: autoRecord", "Catalog.Контрагенты: autoRecord"],
"attributes": ["АдресСервера: String(200)"] }
```
@@ -0,0 +1,90 @@
# Прочие типы
Редкие/служебные объекты. Каждый — минимальный набор свойств.
## FunctionalOption (Функциональная опция)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `location` | пусто | где хранится значение: `Constant.X` / `InformationRegister.X.Resource.Y` / `<Тип>.X.Attribute.Y` |
| `content` | `[]` | реквизиты/измерения/ресурсы, зависящие от опции (полные пути к объектам) |
| `privilegedGetMode` | `true` | bool |
| `comment` | пусто | строка |
```json
{ "type": "FunctionalOption", "name": "ВестиУчетПоСкладам", "location": "Constant.ВестиУчетПоСкладам",
"content": ["Document.РеализацияТоваров.TabularSection.Товары.Attribute.Склад"] }
```
## FilterCriterion (Критерий отбора)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `valueType` | — | тип значения отбора (составной через `+`) |
| `content` | `[]` | реквизиты, по которым идёт отбор (пути к объектам) |
| `useStandardCommands` | `true` | bool |
| `defaultForm` / `auxiliaryForm` | пусто | формы |
| `comment` | пусто | строка |
```json
{ "type": "FilterCriterion", "name": "ДокументыПоКонтрагенту", "valueType": "CatalogRef.Контрагенты",
"content": ["Document.Реализация.Attribute.Контрагент"] }
```
## SettingsStorage (Хранилище настроек)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `defaultSaveForm` / `defaultLoadForm` | пусто | формы сохранения / загрузки |
| `auxiliarySaveForm` / `auxiliaryLoadForm` | пусто | вспомогательные формы |
| `comment` | пусто | строка |
## CommonForm (Общая форма)
Создаёт метаданные + заготовку формы. Содержимое формы наполняется `/form-compile` или `/form-edit`.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `formType` | `Managed` | тип формы |
| `usePurposes` | `[PlatformApplication, MobilePlatformApplication]` | назначение (массив) |
| `useStandardCommands` | `false` | bool |
| `includeHelpInContents` | `false` | bool |
| `comment` | пусто | строка |
```json
{ "type": "CommonForm", "name": "НастройкиОбмена", "usePurposes": ["PlatformApplication"] }
```
## CommonPicture / CommonTemplate (Общие картинки и макеты)
Только метаданные + регистрация; содержимое (`Ext/Picture*`, `Ext/Template.*`) импортируется отдельно
(для табличного макета — `/mxl-compile`).
- **CommonPicture**`availabilityForChoice` / `availabilityForAppearance` (bool, по умолчанию `false`).
- **CommonTemplate**`templateType` (`SpreadsheetDocument` по умолчанию / `TextDocument` / `HTMLDocument` /
`BinaryData` / `AddIn` / `DataCompositionSchema` / `DataCompositionAppearanceTemplate` / `GraphicalSchema`).
```json
{ "type": "CommonTemplate", "name": "ПечатьЗаказа", "templateType": "SpreadsheetDocument" }
```
## Служебные типы
- **SessionParameter** (параметр сеанса) — `valueType` (тип значения, составной через `+`).
- **FunctionalOptionsParameter** (параметр функциональной опции) — `use` (массив измерений/реквизитов).
- **WSReference** (WS-ссылка) — `locationURL` (URL WSDL).
- **CommandGroup** (группа команд) — `category` (по умолч. `NavigationPanel`) — где размещается группа:
`NavigationPanel` / `ActionsPanel` (командный интерфейс раздела) или `FormCommandBar` / `FormNavigationPanel`
(командный интерфейс формы); `representation` (`Auto`), `tooltip` (ML), `picture`. Команды объекта ссылаются на
группу через `group: "CommandGroup.<Имя>"` (см. `blocks.md`).
- **CommonCommand** (общая команда) — `group`, `representation`, `tooltip`, `picture`, `shortcut`,
`commandParameterType`, `parameterUseMode` (`Single`/`Multiple`), `modifiesData`, `includeHelpInContents`.
Создаёт `Ext/CommandModule.bsl`.
- **CommonAttribute** (общий реквизит) — `valueType` (по умолчанию `String(0)`) + свойства реквизита
(`attributes.md`) + `content` (объекты, куда входит реквизит) + свойства разделения данных
(`dataSeparation`, `separatedDataUse`, `usersSeparation`, … — по умолчанию `DontUse`/`Independently`).
```json
{ "type": "CommonAttribute", "name": "Организация", "valueType": "CatalogRef.Организации",
"autoUse": "Use", "content": ["Document.РеализацияТоваров", "Document.ПоступлениеТоваров"] }
```
@@ -0,0 +1,49 @@
# BusinessProcess, Task (Бизнес-процессы и Задачи)
Ссылочные типы. Наследуют слой Catalog (нумерация, формы, `standardAttributes`, `characteristics`, `basedOn`,
представления — см. `catalog.md` / `attributes.md` / `blocks.md`). Бизнес-процесс всегда связан с задачей.
## BusinessProcess (Бизнес-процесс)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `task` | пусто | ссылка на задачу `Task.X` (обязательна для рабочего БП) |
| `numberType` | `String` | `String` / `Number` |
| `numberLength` | `11` | длина номера |
| `checkUnique` | `true` | bool |
| `autonumbering` | `true` | bool |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
Создаётся с картой маршрута (`Ext/Flowchart.xml`) и модулем объекта.
```json
{ "type": "BusinessProcess", "name": "Согласование", "task": "Task.ЗадачаИсполнителя",
"attributes": ["Документ: DocumentRef.ЗаявкаНаРасход"] }
```
## Task (Задача)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `numberType` | `String` | `String` / `Number` |
| `numberLength` | `14` | длина номера |
| `checkUnique` | `true` | bool |
| `autonumbering` | `true` | bool |
| `descriptionLength` | `150` | длина наименования |
| `addressing` | пусто | ссылка на регистр сведений адресации `InformationRegister.X` |
| `mainAddressingAttribute` | пусто | основной реквизит адресации (имя реквизита адресации) |
| `currentPerformer` | пусто | реквизит текущего исполнителя |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `addressingAttributes` | `[]` | реквизиты адресации (см. ниже) |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
**Реквизит адресации** — shorthand `"Имя: Тип"` или объект `{ name, type, addressingDimension }`
(`addressingDimension` — измерение регистра адресации).
```json
{ "type": "Task", "name": "ЗадачаИсполнителя",
"addressingAttributes": ["Исполнитель: CatalogRef.Пользователи", "Роль: CatalogRef.Роли"] }
```
@@ -0,0 +1,76 @@
# Регистры: Information, Accumulation, Accounting, Calculation
**Измерения и ресурсы** задаются как реквизиты (shorthand `"Имя: Тип | флаги"` или объектная форма, см.
`attributes.md`). Флаги измерений: `master`, `mainFilter`, `denyIncomplete`, `useInTotals` (регистр накопления).
```json
"dimensions": ["Организация: CatalogRef.Организации | master, mainFilter"],
"resources": ["Сумма: Number(15,2)"]
```
## InformationRegister (Регистр сведений)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `writeMode` | `Independent` | `Independent` / `RecorderSubordinate` |
| `periodicity` | `Nonperiodical` | `Nonperiodical` / `Second` / `Day` / `Month` / `Quarter` / `Year` / `RecorderPosition` |
| `mainFilterOnPeriod` | `false` | bool (основной отбор по периоду) |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
```json
{ "type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] }
```
## AccumulationRegister (Регистр накопления)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `registerType` | `Balance` | `Balance` (остатки) / `Turnovers` (обороты) |
| `enableTotalsSplitting` | `true` | bool (разделение итогов) |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
```json
{ "type": "AccumulationRegister", "name": "ОстаткиТоваров", "registerType": "Balance",
"dimensions": ["Номенклатура: CatalogRef.Номенклатура", "Склад: CatalogRef.Склады"],
"resources": ["Количество: Number(15,3)"] }
```
## AccountingRegister (Регистр бухгалтерии)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `chartOfAccounts` | — | **обязательно**: ссылка на план счетов `ChartOfAccounts.X` |
| `correspondence` | `false` | bool (корреспонденция) |
| `periodAdjustmentLength` | `0` | длина периода корректировки |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
```json
{ "type": "AccountingRegister", "name": "Хозрасчетный",
"chartOfAccounts": "ChartOfAccounts.Хозрасчетный",
"dimensions": ["Организация: CatalogRef.Организации"], "resources": ["Сумма: Number(15,2)"] }
```
## CalculationRegister (Регистр расчёта)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `chartOfCalculationTypes` | — | **обязательно**: ссылка на ПВР `ChartOfCalculationTypes.X` |
| `periodicity` | `Month` | периодичность |
| `actionPeriod` | `false` | bool (период действия) |
| `basePeriod` | `false` | bool (базовый период) |
| `schedule` | пусто | ссылка на регистр сведений графиков |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
```json
{ "type": "CalculationRegister", "name": "Начисления",
"chartOfCalculationTypes": "ChartOfCalculationTypes.Начисления", "periodicity": "Month",
"dimensions": ["Сотрудник: CatalogRef.Сотрудники"], "resources": ["Сумма: Number(15,2)"] }
```
> **AccountingRegister** требует план счетов, **CalculationRegister** — план видов расчёта (и оба — документ-регистратор).
@@ -0,0 +1,45 @@
# Report, DataProcessor (Отчёты и Обработки)
Почти идентичны по составу: реквизиты, табличные части, формы, макеты, команды. Модуль объекта — `Ext/ObjectModule.bsl`.
Реквизиты и ТЧ — `attributes.md`; команды — `blocks.md`.
Ссылки на формы/схемы/хранилища пишутся **как есть** (имя формы может быть буквально «Форма»).
## Report (Отчёт)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `useStandardCommands` | `true` | bool (доступность через стандартный командный интерфейс) |
| `mainDataCompositionSchema` | пусто | основной макет СКД (`Report.X.Template.ОсновнаяСхемаКомпоновкиДанных`) |
| `defaultForm` / `auxiliaryForm` | пусто | основная / вспомогательная форма |
| `defaultSettingsForm` / `auxiliarySettingsForm` / `defaultVariantForm` | пусто | формы настроек / вариантов |
| `variantsStorage` / `settingsStorage` | пусто | хранилища вариантов / настроек (`SettingsStorage.X`) |
| `extendedPresentation` / `explanation` | пусто | представление / пояснение (ML) |
| `includeHelpInContents` | `false` | bool |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
```json
{ "type": "Report", "name": "АнализПродаж", "useStandardCommands": false,
"mainDataCompositionSchema": "Report.АнализПродаж.Template.ОсновнаяСхемаКомпоновкиДанных",
"attributes": ["Период: StandardPeriod"] }
```
## DataProcessor (Обработка)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `useStandardCommands` | `true` | bool |
| `defaultForm` / `auxiliaryForm` | пусто | основная / вспомогательная форма |
| `extendedPresentation` / `explanation` | пусто | представление / пояснение (ML) |
| `includeHelpInContents` | `false` | bool |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
```json
{ "type": "DataProcessor", "name": "ЗагрузкаТаблиц", "useStandardCommands": false,
"attributes": [{ "name": "Таблица", "type": "ValueTree" }, { "name": "Произвольные", "type": "" }] }
```
> Реквизиты отчётов/обработок допускают платформенные типы-коллекции: `ValueTable`, `ValueTree`, `ValueList`,
> `StandardPeriod`, `SpreadsheetDocument` и др., а также `"type": ""` — реквизит без типа.
@@ -0,0 +1,40 @@
# Enum, Constant, DefinedType
## Enum (Перечисление)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `values` | `[]` | значения перечисления (массив имён или объектов) |
Значение — строка `"ИмяЗначения"` или объект `{ name, synonym }`.
```json
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "ВРаботе", "Закрыт"] }
```
## Constant (Константа)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `valueType` | `String` | тип значения (shorthand типа) |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
`valueType` принимает shorthand: `"String(100)"`, `"Number(15,2)"`, `"Boolean"`, `"CatalogRef.Валюты"`,
составной через `+`.
```json
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
```
## DefinedType (Определяемый тип)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `valueTypes` | `[]` | состав типа (массив shorthand-типов) |
| `valueType` | — | то же одной строкой (`"A + B"`) или строкой одного типа |
```json
{ "type": "DefinedType", "name": "ДенежныеСредства",
"valueTypes": ["CatalogRef.БанковскиеСчета", "CatalogRef.Кассы"] }
{ "type": "DefinedType", "name": "ФлагАктивности", "valueType": "Boolean" }
```
@@ -1,116 +0,0 @@
# Базовые типы: Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
## Catalog
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `hierarchical` | `false` | Hierarchical |
| `hierarchyType` | `HierarchyFoldersAndItems` | HierarchyType |
| `limitLevelCount` | `false` | LimitLevelCount |
| `levelCount` | `2` | LevelCount |
| `foldersOnTop` | `true` | FoldersOnTop |
| `codeLength` | `9` | CodeLength |
| `codeType` | `String` | CodeType |
| `codeAllowedLength` | `Variable` | CodeAllowedLength |
| `codeSeries` | `WholeCatalog` | CodeSeries |
| `descriptionLength` | `25` | DescriptionLength |
| `autonumbering` | `true` | Autonumbering |
| `checkUnique` | `false` | CheckUnique |
| `defaultPresentation` | `AsDescription` | DefaultPresentation |
| `subordinationUse` | `ToItems` | SubordinationUse |
| `quickChoice` | `false` | QuickChoice |
| `choiceMode` | `BothWays` | ChoiceMode |
| `owners` | `[]` | Owners |
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
```json
{ "type": "Catalog", "name": "Организации", "attributes": ["ИНН: String(12)", "КПП: String(9)"] }
```
## Document
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `numberType` | `String` | NumberType |
| `numberLength` | `11` | NumberLength |
| `numberAllowedLength` | `Variable` | NumberAllowedLength |
| `numberPeriodicity` | `Year` | NumberPeriodicity |
| `checkUnique` | `true` | CheckUnique |
| `autonumbering` | `true` | Autonumbering |
| `posting` | `Allow` | Posting |
| `realTimePosting` | `Deny` | RealTimePosting |
| `registerRecordsDeletion` | `AutoDelete` | RegisterRecordsDeletion |
| `registerRecordsWritingOnPost` | `WriteModified` | RegisterRecordsWritingOnPost |
| `postInPrivilegedMode` | `true` | PostInPrivilegedMode |
| `unpostInPrivilegedMode` | `true` | UnpostInPrivilegedMode |
| `registerRecords` | `[]` | RegisterRecords |
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
RegisterRecords — массив строк: `"AccumulationRegister.Продажи"`, `"InformationRegister.Цены"`.
```json
{
"type": "Document", "name": "ПриходнаяНакладная",
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] }
}
```
## Enum
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `values` | `[]` | → EnumValue в ChildObjects |
```json
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "ВРаботе", "Закрыт"] }
```
## Constant
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `valueType` | `String` | Type |
`valueType` принимает shorthand типа: `"String(100)"`, `"Number(15,2)"`, `"Boolean"`, `"CatalogRef.Валюты"`.
```json
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
```
## DefinedType
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `valueTypes` | `[]` | Type (составной тип) |
| `valueType` | — | Алиас для `valueTypes` (строка или массив) |
```json
{ "type": "DefinedType", "name": "ДенежныеСредства", "valueTypes": ["CatalogRef.БанковскиеСчета", "CatalogRef.Кассы"] }
{ "type": "DefinedType", "name": "ФлагАктивности", "valueType": "Boolean" }
```
## Report
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
```json
{ "type": "Report", "name": "ОстаткиТоваров" }
```
## DataProcessor
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
```json
{ "type": "DataProcessor", "name": "ЗагрузкаДанных", "attributes": ["ПутьКФайлу: String(500)"] }
```
@@ -1,136 +0,0 @@
# Процессы и сервисные: BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal
## BusinessProcess
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `task` | `""` | Task (ссылка `Task.XXX`) |
| `numberType` | `String` | NumberType |
| `numberLength` | `11` | NumberLength |
| `checkUnique` | `true` | CheckUnique |
| `autonumbering` | `true` | Autonumbering |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
Модули: `Ext/ObjectModule.bsl`, `Ext/Flowchart.xml`.
```json
{ "type": "BusinessProcess", "name": "Задание", "task": "Task.ЗадачаИсполнителя", "attributes": ["Описание: String(200)"] }
```
## Task
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `numberType` | `String` | NumberType |
| `numberLength` | `14` | NumberLength |
| `checkUnique` | `true` | CheckUnique |
| `autonumbering` | `true` | Autonumbering |
| `descriptionLength` | `150` | DescriptionLength |
| `addressing` | `""` | Addressing (ссылка на РС адресации) |
| `mainAddressingAttribute` | `""` | MainAddressingAttribute |
| `currentPerformer` | `""` | CurrentPerformer |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
| `addressingAttributes` | `[]` | → AddressingAttribute (shorthand или объект) |
AddressingAttribute — shorthand `"Имя: Тип"` или объект `{ "name", "type", "addressingDimension" }`.
```json
{
"type": "Task", "name": "ЗадачаИсполнителя",
"addressingAttributes": ["Исполнитель: CatalogRef.Пользователи"]
}
```
## ExchangePlan
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `codeLength` | `9` | CodeLength |
| `descriptionLength` | `100` | DescriptionLength |
| `distributedInfoBase` | `false` | DistributedInfoBase |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
Модули: `Ext/ObjectModule.bsl`, `Ext/Content.xml`.
```json
{ "type": "ExchangePlan", "name": "ОбменССайтом", "attributes": ["АдресСервера: String(200)"] }
```
## CommonModule
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `context` | — | Шорткат (см. ниже) |
| `global` | `false` | Global |
| `server` | `false` | Server |
| `serverCall` | `false` | ServerCall |
| `clientManagedApplication` | `false` | ClientManagedApplication |
| `externalConnection` | `false` | ExternalConnection |
| `privileged` | `false` | Privileged |
| `returnValuesReuse` | `DontUse` | ReturnValuesReuse |
Шорткаты `context`: `"server"` → Server+ServerCall, `"client"` → ClientManagedApplication, `"serverClient"` → Server+ClientManagedApplication.
```json
{ "type": "CommonModule", "name": "ОбщиеФункции", "context": "serverClient" }
```
## ScheduledJob
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `methodName` | `""` | MethodName |
| `description` | = synonym | Description |
| `use` | `false` | Use |
| `predefined` | `false` | Predefined |
| `restartCountOnFailure` | `3` | RestartCountOnFailure |
| `restartIntervalOnFailure` | `10` | RestartIntervalOnFailure |
Формат `methodName`: `"МодульСервер.Процедура"` — авто-дополняется до `CommonModule.МодульСервер.Процедура`.
```json
{ "type": "ScheduledJob", "name": "ОбменДанными", "methodName": "ОбменДаннымиСервер.Выполнить" }
```
## EventSubscription
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `source` | `[]` | Source (массив, формат `XxxObject.Name`) |
| `event` | `BeforeWrite` | Event |
| `handler` | `""` | Handler |
Формат `handler`: `"МодульСервер.Процедура"` — авто-дополняется до `CommonModule.МодульСервер.Процедура`.
Значения `event`: `BeforeWrite`, `OnWrite`, `BeforeDelete`, `OnReadAtServer`, `FillCheckProcessing`.
Формат `source`: `"CatalogObject.Xxx"`, `"DocumentObject.Xxx"`.
```json
{ "type": "EventSubscription", "name": "ПередЗаписью", "source": ["CatalogObject.Контрагенты"], "event": "BeforeWrite", "handler": "ОбщиеФункции.ПередЗаписью" }
```
## DocumentJournal
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `registeredDocuments` | `[]` | RegisteredDocuments (массив `"Document.Xxx"`) |
| `columns` | `[]` | → Column |
Колонки — строка `"Имя"` или объект `{ "name", "synonym", "indexing": "Index"/"DontIndex", "references": ["Document.Xxx.Attribute.Yyy"] }`.
```json
{
"type": "DocumentJournal", "name": "Взаимодействия",
"registeredDocuments": ["Document.Встреча", "Document.Звонок"],
"columns": [{ "name": "Организация", "indexing": "Index", "references": ["Document.Встреча.Attribute.Организация"] }]
}
```
## Зависимости
- **ScheduledJob/EventSubscription** — процедура-обработчик должна существовать в модуле (экспортная)
- **BusinessProcess**`Task` (задача должна существовать)
@@ -1,174 +0,0 @@
# Регистры и планы: InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
## Измерения и ресурсы (общее)
Синтаксис аналогичен реквизитам (shorthand `"Имя: Тип | флаги"`).
Флаги измерений: `master`, `mainFilter`, `denyIncomplete`, `useInTotals` (AccumulationRegister only, default `true`).
```json
"dimensions": ["Организация: CatalogRef.Организации | master, mainFilter, denyIncomplete"],
"resources": ["Сумма: Number(15,2)"]
```
---
## InformationRegister
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `writeMode` | `Independent` | WriteMode |
| `periodicity` | `Nonperiodical` | InformationRegisterPeriodicity |
| `mainFilterOnPeriod` | авто* | MainFilterOnPeriod |
| `dimensions` | `[]` | → Dimension |
| `resources` | `[]` | → Resource |
| `attributes` | `[]` | → Attribute |
\* `mainFilterOnPeriod` = `true` если `periodicity` != `Nonperiodical`.
```json
{
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"]
}
```
## AccumulationRegister
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `registerType` | `Balance` | RegisterType (`Balance` / `Turnovers`) |
| `enableTotalsSplitting` | `true` | EnableTotalsSplitting |
| `dimensions` | `[]` | → Dimension |
| `resources` | `[]` | → Resource |
| `attributes` | `[]` | → Attribute |
```json
{
"type": "AccumulationRegister", "name": "ОстаткиТоваров", "registerType": "Balance",
"dimensions": ["Номенклатура: CatalogRef.Номенклатура"],
"resources": ["Количество: Number(15,3)"]
}
```
## AccountingRegister
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `chartOfAccounts` | `""` | ChartOfAccounts (**обязательная** ссылка на план счетов) |
| `correspondence` | `false` | Correspondence |
| `periodAdjustmentLength` | `0` | PeriodAdjustmentLength |
| `dimensions` | `[]` | → Dimension |
| `resources` | `[]` | → Resource |
| `attributes` | `[]` | → Attribute |
```json
{
"type": "AccountingRegister", "name": "Хозрасчетный",
"chartOfAccounts": "ChartOfAccounts.Хозрасчетный",
"dimensions": ["Организация: CatalogRef.Организации"],
"resources": ["Сумма: Number(15,2)"]
}
```
## CalculationRegister
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `chartOfCalculationTypes` | `""` | ChartOfCalculationTypes (**обязательная** ссылка на ПВР) |
| `periodicity` | `Month` | Periodicity |
| `actionPeriod` | `false` | ActionPeriod |
| `basePeriod` | `false` | BasePeriod |
| `schedule` | `""` | Schedule (ссылка на РС графиков) |
| `dimensions` | `[]` | → Dimension |
| `resources` | `[]` | → Resource |
| `attributes` | `[]` | → Attribute |
```json
{
"type": "CalculationRegister", "name": "Начисления",
"chartOfCalculationTypes": "ChartOfCalculationTypes.Начисления",
"periodicity": "Month",
"dimensions": ["Сотрудник: CatalogRef.Сотрудники"],
"resources": ["Сумма: Number(15,2)"]
}
```
---
## ChartOfCharacteristicTypes
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `codeLength` | `9` | CodeLength |
| `descriptionLength` | `25` | DescriptionLength |
| `autonumbering` | `true` | Autonumbering |
| `checkUnique` | `false` | CheckUnique |
| `characteristicExtValues` | `""` | CharacteristicExtValues |
| `valueTypes` | авто* | Type (составной тип значений характеристик) |
| `hierarchical` | `false` | Hierarchical |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
\* По умолчанию: Boolean, String(100), Number(15,2), DateTime.
```json
{
"type": "ChartOfCharacteristicTypes", "name": "ВидыСубконто",
"valueTypes": ["CatalogRef.Номенклатура", "CatalogRef.Контрагенты", "Boolean", "String", "Number(15,2)"]
}
```
## ChartOfAccounts
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `extDimensionTypes` | `""` | ExtDimensionTypes (ссылка на ПВХ) |
| `maxExtDimensionCount` | `3` | MaxExtDimensionCount |
| `codeMask` | `""` | CodeMask |
| `codeLength` | `8` | CodeLength |
| `descriptionLength` | `120` | DescriptionLength |
| `codeSeries` | `WholeChartOfAccounts` | CodeSeries |
| `autoOrderByCode` | `true` | AutoOrderByCode |
| `orderLength` | `5` | OrderLength |
| `hierarchical` | `false` | Hierarchical |
| `accountingFlags` | `[]` | → AccountingFlag (Boolean-тип, массив имён) |
| `extDimensionAccountingFlags` | `[]` | → ExtDimensionAccountingFlag (Boolean-тип, массив имён) |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
```json
{
"type": "ChartOfAccounts", "name": "Хозрасчетный",
"extDimensionTypes": "ChartOfCharacteristicTypes.ВидыСубконто", "maxExtDimensionCount": 3,
"codeLength": 8, "codeMask": "@@@.@@.@",
"accountingFlags": ["Валютный", "Количественный"],
"extDimensionAccountingFlags": ["Суммовой", "Валютный"]
}
```
## ChartOfCalculationTypes
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `codeLength` | `9` | CodeLength |
| `descriptionLength` | `25` | DescriptionLength |
| `autonumbering` | `true` | Autonumbering |
| `checkUnique` | `false` | CheckUnique |
| `dependenceOnCalculationTypes` | `DontUse` | DependenceOnCalculationTypes |
| `actionPeriodUse` | `false` | ActionPeriodUse |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
`dependenceOnCalculationTypes`: `DontUse`, `OnActionPeriod`.
```json
{ "type": "ChartOfCalculationTypes", "name": "Начисления", "dependenceOnCalculationTypes": "OnActionPeriod" }
```
## Зависимости
- **AccountingRegister** требует `ChartOfAccounts` (и документ-регистратор)
- **CalculationRegister** требует `ChartOfCalculationTypes` (и документ-регистратор)
- **ChartOfAccounts** ссылается на `ChartOfCharacteristicTypes` через `extDimensionTypes`
@@ -1,103 +0,0 @@
# Веб-сервисы: HTTPService, WebService
## HTTPService
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `rootURL` | `= name.toLower()` | RootURL |
| `reuseSessions` | `DontUse` | ReuseSessions |
| `sessionMaxAge` | `20` | SessionMaxAge |
| `urlTemplates` | `{}` | → URLTemplate |
Модули: `Ext/Module.bsl`.
### urlTemplates — вложенная структура
`urlTemplates` — объект `{ "TemplateName": templateDef, ... }`.
Каждый `templateDef`:
- Строка — URL-шаблон: `"/v1/users"` (без методов)
- Объект:
| Поле | Умолчание | Описание |
|------|----------|----------|
| `template` | `"/templatename"` | URL-путь (с параметрами `{id}`) |
| `methods` | `{}` | Методы: `{ "MethodName": "HTTPMethod" }` |
Допустимые HTTPMethod: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`.
Обработчик метода генерируется автоматически: `{TemplateName}{MethodName}` — должен быть реализован в `Ext/Module.bsl`.
```json
{
"type": "HTTPService", "name": "API", "rootURL": "api",
"urlTemplates": {
"Users": {
"template": "/v1/users/{id}",
"methods": { "Get": "GET", "Create": "POST", "Update": "PUT", "Delete": "DELETE" }
},
"Health": "/health"
}
}
```
## WebService
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `namespace` | `""` | Namespace (URI пространства имён WSDL) |
| `xdtoPackages` | `""` | XDTOPackages |
| `reuseSessions` | `DontUse` | ReuseSessions |
| `sessionMaxAge` | `20` | SessionMaxAge |
| `operations` | `{}` | → Operation |
Модули: `Ext/Module.bsl`.
### operations — вложенная структура
`operations` — объект `{ "OperationName": operationDef, ... }`.
Каждый `operationDef`:
- Строка — тип возврата: `"xs:boolean"` (параметров нет, обработчик = имя операции)
- Объект:
| Поле | Умолчание | Описание |
|------|----------|----------|
| `returnType` | `xs:string` | XDTO-тип возврата |
| `nillable` | `false` | Может ли вернуть null |
| `transactioned` | `false` | Выполнять в транзакции |
| `handler` | `= operationName` | Имя процедуры в модуле |
| `parameters` | `{}` | Параметры операции |
### parameters — параметры операции
`parameters` — объект `{ "ParamName": paramDef, ... }`.
Каждый `paramDef`:
- Строка — XDTO-тип: `"xs:string"` (direction = In, nillable = true)
- Объект:
| Поле | Умолчание | Описание |
|------|----------|----------|
| `type` | `xs:string` | XDTO-тип параметра |
| `nillable` | `true` | Может ли быть null |
| `direction` | `In` | Направление: `In`, `Out`, `InOut` |
Стандартные XDTO-типы: `xs:string`, `xs:boolean`, `xs:int`, `xs:long`, `xs:decimal`, `xs:dateTime`, `xs:base64Binary`.
```json
{
"type": "WebService", "name": "DataExchange",
"namespace": "http://www.1c.ru/DataExchange",
"operations": {
"TestConnection": {
"returnType": "xs:boolean",
"handler": "ПроверкаПодключения",
"parameters": {
"ErrorMessage": { "type": "xs:string", "direction": "Out" }
}
},
"GetVersion": "xs:string"
}
}
```
@@ -0,0 +1,57 @@
# HTTPService, WebService (Веб-сервисы)
Модуль обоих — `Ext/Module.bsl`, в нём реализуются обработчики.
## HTTPService (HTTP-сервис)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `rootURL` | `= name` (в нижнем регистре) | корневой URL |
| `reuseSessions` | `DontUse` | `DontUse` / `AutoUse` |
| `sessionMaxAge` | `20` | время жизни сессии, сек |
| `urlTemplates` | `{}` | шаблоны URL (см. ниже) |
`urlTemplates` — объект `{ "ИмяШаблона": def }`, где `def`:
- строка — URL-путь без методов: `"/health"`;
- объект: `template` (путь с параметрами `{id}`, по умолчанию `/имяшаблона`), `methods``{ "ИмяМетода": "HTTPMethod" }`.
HTTP-методы: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`.
Обработчик метода в модуле именуется `{ИмяШаблона}{ИмяМетода}`.
```json
{ "type": "HTTPService", "name": "API", "rootURL": "api",
"urlTemplates": {
"Users": { "template": "/v1/users/{id}", "methods": { "Get": "GET", "Create": "POST", "Delete": "DELETE" } },
"Health": "/health"
} }
```
## WebService (Веб-сервис, SOAP)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `namespace` | пусто | URI пространства имён WSDL |
| `xdtoPackages` | пусто | XDTO-пакеты |
| `reuseSessions` | `DontUse` | `DontUse` / `AutoUse` |
| `sessionMaxAge` | `20` | время жизни сессии, сек |
| `operations` | `{}` | операции (см. ниже) |
`operations` — объект `{ "ИмяОперации": def }`, где `def`:
- строка — XDTO-тип возврата без параметров: `"xs:string"`;
- объект: `returnType` (по умолчанию `xs:string`), `nillable` (bool), `transactioned` (bool),
`handler` (имя процедуры, по умолчанию = имя операции), `parameters`.
`parameters` — объект `{ "ИмяПараметра": def }`, где `def`:
- строка — XDTO-тип (`direction` = `In`);
- объект: `type` (по умолчанию `xs:string`), `nillable` (bool, по умолчанию `true`), `direction` (`In` / `Out` / `InOut`).
XDTO-типы: `xs:string`, `xs:boolean`, `xs:int`, `xs:long`, `xs:decimal`, `xs:dateTime`, `xs:base64Binary`.
```json
{ "type": "WebService", "name": "DataExchange", "namespace": "http://www.1c.ru/DataExchange",
"operations": {
"TestConnection": { "returnType": "xs:boolean", "handler": "ПроверкаПодключения",
"parameters": { "ErrorMessage": { "type": "xs:string", "direction": "Out" } } },
"GetVersion": "xs:string"
} }
```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+52
View File
@@ -0,0 +1,52 @@
---
name: meta-decompile
description: Декомпиляция объекта метаданных 1С в JSON-заготовку формата meta-compile. Используй когда нужно получить черновик DSL-описания нового объекта по образцу другого. Не сохраняет UUID/модули/формы.
argument-hint: <ObjectPath> [-OutputPath <out.json>]
disable-model-invocation: true
allowed-tools:
- Bash
- Read
- Write
- Glob
---
# /meta-decompile — DSL-заготовка из XML объекта метаданных
Читает XML объекта метаданных (`Catalogs/Имя.xml` и т.п.) и эмитит компактный JSON в формате `/meta-compile`. Назначение — **взять существующий объект образцом и собрать по нему НОВЫЙ**: декомпилировать → поправить → скомпилировать под другим именем.
## ⚠️ Главное: это НЕ обратимая выгрузка
Компиляция черновика создаёт **новый объект с новой идентичностью**, а не копию исходного. В JSON **не** попадают: UUID (идентичность самого объекта и всех дочерних), тела модулей, формы, макеты, права. Захватываются только структура и свойства.
Отсюда правило: **никогда не компилируй черновик поверх объекта-источника и не выдавай его за «реимпорт»** — у пересобранного объекта другие UUID, поэтому все ссылки на исходный объект (из кода, других объектов, состава подсистем, предопределённых данных) сломаются, а код модулей и формы пропадут.
## Когда использовать
**Собрать новый объект по образцу существующего** — получить DSL-заготовку рабочего объекта, переименовать и адаптировать состав, скомпилировать в новый. Быстрее, чем писать DSL с нуля для богатого объекта.
## Когда **не** использовать
- **Точечная правка существующего объекта** (добавить реквизит, ТЧ, свойство) → `/meta-edit`. Цикл decompile→compile тут вреден: даёт объект с новой идентичностью и теряет модули/формы.
- **Сохранить / восстановить / перенести тот же объект** (бэкап, миграция между конфигурациями с сохранением ссылок) → штатная выгрузка 1С (`/db-dump-xml``/db-load-xml`, CF), а не decompile.
- **Просто понять структуру** объекта (реквизиты, ТЧ, типы) без пересборки → `/meta-info` (дешевле, не плодит файл).
## Параметры
| Параметр | Описание |
|----------|----------|
| `ObjectPath` | Путь к XML объекта (`Catalogs/Имя.xml`), обязательный |
| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-decompile.ps1" -ObjectPath "<Объект.xml>" -OutputPath "<out.json>"
```
Неподдерживаемый тип объекта или не-`MetaDataObject` root → ненулевой код выхода и сообщение в stderr.
## Workflow (сборка нового объекта по образцу)
1. `/meta-decompile <Образец.xml> -OutputPath draft.json` — получить заготовку.
2. В `draft.json` **сменить `name`** на имя нового объекта и адаптировать состав (реквизиты/ТЧ/свойства). Ссылки на *другие* объекты (владельцы, ввод на основании, типы) — по имени, сохраняются как есть.
3. `/meta-compile -JsonPath draft.json -OutputDir <ConfigDir>` — собрать (объект получит свежие UUID).
4. `/meta-validate` + `/meta-info` — проверить.
5. Модули, формы, макеты, права — добавить отдельно (в черновик они не попадают).
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3
View File
@@ -59,6 +59,9 @@ Batch через `;;` во всех операциях. Подробный си
Позиционная вставка: `"Склад: CatalogRef.Склады >> after Организация"`. Позиционная вставка: `"Склад: CatalogRef.Склады >> after Организация"`.
`modify-attribute` умеет и структурные свойства реквизита — формат/подсказку, форму и параметры выбора,
значение заполнения, границы (`Format`, `ChoiceForm`, `ChoiceParameters`, `FillValue`, `MinValue`/`MaxValue` и др.).
### Свойства объекта — [properties-reference.md](properties-reference.md) ### Свойства объекта — [properties-reference.md](properties-reference.md)
| Операция | Формат Value | Пример | | Операция | Формат Value | Пример |
+48 -3
View File
@@ -11,7 +11,7 @@
**Shorthand-формат** реквизитов: `ИмяРеквизита: Тип | флаги` **Shorthand-формат** реквизитов: `ИмяРеквизита: Тип | флаги`
Флаги: `req` (FillChecking=ShowError), `index` (Indexing=Index), `master` (Master=true, только dimensions), `mainFilter` (MainFilterOperand, только dimensions). Флаги: `req` — обязательное заполнение; `index` — индексировать; `master` — ведущее измерение (только dimensions); `mainFilter` — основной отбор (только dimensions).
**Позиционная вставка**: `>> after ИмяЭлемента` или `<< before ИмяЭлемента`: **Позиционная вставка**: `>> after ИмяЭлемента` или `<< before ИмяЭлемента`:
```powershell ```powershell
@@ -84,6 +84,27 @@ Batch через `;;` — можно указать разные ТЧ: `"Тов
Формат аналогичен `modify-attribute`: `ИмяТЧ: ключ=значение, ключ=значение`. Формат аналогичен `modify-attribute`: `ИмяТЧ: ключ=значение, ключ=значение`.
## add-predefined
Добавить предопределённые элементы (Catalog, ChartOfCharacteristicTypes). Существующие элементы и их
идентификаторы сохраняются, новые получают свежий id.
Inline — строка `(Код) Имя [Наименование]` (batch через `;;`; `[Наименование]` необязательно — иначе авто из имени):
```powershell
-Operation add-predefined -Value "(001) Основной ;; (002) Резервный [Резервный склад]"
```
JSON — строки и/или объекты (для групп с вложенными):
```json
{ "add": { "predefined": [
"(001) Основной",
{ "name": "Группа", "isFolder": true, "childItems": ["(002) Вложенный"] }
] } }
```
Ключи объекта: `name`, `code`, `description`, `isFolder`, `childItems` (дерево). Тип кода (строковый/числовой)
берётся из объекта автоматически.
## add-enumValue / add-form / add-template / add-command ## add-enumValue / add-form / add-template / add-command
Просто имена (batch через `;;`): Просто имена (batch через `;;`):
@@ -107,10 +128,34 @@ Batch через `;;` — можно указать разные ТЧ: `"Тов
Формат: `ИмяЭлемента: ключ=значение, ключ=значение` Формат: `ИмяЭлемента: ключ=значение, ключ=значение`
Ключи: `name` (rename), `type`, `synonym`, `indexing`, `fillChecking`, `use` и др. **Спец-операции** (строчные ключи): `name` (переименование), `type` (смена типа), `synonym`.
**Свойства** задавайте по имени свойства 1С (PascalCase, как в конфигураторе): `Indexing`, `FillChecking`,
`Use`, `FullTextSearch`, `DataHistory`, `PasswordMode`, `MultiLine`, `Mask`, `CreateOnInput`, `QuickChoice` и др.
Свойство можно задать, даже если у реквизита оно ещё не выставлено. Опечатка в имени свойства → ошибка
(правка не теряется молча).
```powershell ```powershell
-Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)" -Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)"
-Operation modify-attribute -Value "Комментарий: indexing=Index" -Operation modify-attribute -Value "Комментарий: Indexing=Index, FullTextSearch=Use"
-Operation modify-enumValue -Value "СтароеЗначение: name=НовоеЗначение" -Operation modify-enumValue -Value "СтароеЗначение: name=НовоеЗначение"
``` ```
### Структурные свойства реквизита
Свойства со сложным значением задавайте через JSON DSL (`{ "modify": { "attributes": { "Имя": { ... } } } }`):
| Ключ | Значение | Пример (JSON) |
|------|----------|---------------|
| `Format` / `EditFormat` / `ToolTip` | строка (мультиязычная) | `"Format": "ДФ=dd.MM.yyyy"` |
| `ChoiceForm` | путь формы выбора | `"ChoiceForm": "Catalog.Товары.Form.ФормаВыбора"` |
| `MinValue` / `MaxValue` | число или строка | `"MinValue": 0, "MaxValue": 100` |
| `FillValue` | значение заполнения | `"FillValue": "EmptyRef"` · `true` · `10` · `{"nil": true}` |
| `LinkByType` | `{dataPath, linkItem?}` | `"LinkByType": {"dataPath": "Вид", "linkItem": 0}` |
| `ChoiceParameterLinks` | `[{name, dataPath, valueChange?}]` | `["Отбор.Организация=Организация"]` |
| `ChoiceParameters` | `[{name, type?, value?}]` | `[{"name": "Отбор.ЭтоГруппа", "value": false}]` |
- `FillValue`: `"EmptyRef"` — пустая ссылка по типу реквизита; `{"emptyRef": true}` / `{"nil": true}` — явные маркеры.
- `ChoiceParameters` value — булево/число/строка/ссылочный путь или массив; укажите `type` (напр.
`EnumRef.СтавкиНДС`), чтобы задавать значения короткими именами (`"Оптовая"` вместо полного пути).
- В путях данных (`LinkByType`/`ChoiceParameterLinks`) можно писать короткое имя реквизита вместо полного пути.
@@ -1,18 +1,32 @@
# Свойства объекта и complex properties # Свойства объекта и свойства-списки
Справочник операций для скалярных свойств объекта и свойств со вложенной XML-структурой (Owners, RegisterRecords, BasedOn, InputByString). Справочник операций для обычных свойств объекта и свойств-списков (Owners, RegisterRecords, BasedOn, InputByString).
## modify-property ## modify-property
Изменение скалярных свойств объекта. Формат: `Ключ=Значение` (batch через `;;`): Изменение свойств объекта по имени свойства 1С (PascalCase, как в конфигураторе). Формат: `Ключ=Значение`
(batch через `;;`):
```powershell ```powershell
-Operation modify-property -Value "CodeLength=11 ;; DescriptionLength=150" -Operation modify-property -Value "CodeLength=11 ;; DescriptionLength=150"
-Operation modify-property -Value "Hierarchical=true" -Operation modify-property -Value "Hierarchical=true"
``` ```
## Complex properties Свойство можно задать, даже если оно ещё не выставлено у объекта (например `FullTextSearch`, `DataHistory`).
Опечатка в имени свойства → ошибка (правка не теряется молча). Допустимы имена свойств соответствующего типа объекта.
Свойства со вложенной XML-структурой. Поддерживаются через inline `add-*` / `remove-*` / `set-*` и через JSON `modify.properties`. ### Type — тип значения (Константа, ПВХ)
`Type=...` перестраивает дескриптор типа значения. Значение — тип 1С в том же синтаксисе,
что у реквизитов: составной через `+`, с квалификаторами и ссылочными типами:
```powershell
-Operation modify-property -Value "Type=String(100) + Number(15,2) + CatalogRef.Номенклатура"
```
Структурные свойства (со вложенными узлами) в скалярный текст не превращаются: попытка задать
такое свойство обычным `Ключ=Значение` (кроме `Type`) завершается ошибкой до записи файла.
## Свойства-списки
Свойства, значение которых — список ссылок. Управляются через inline `add-*` / `remove-*` / `set-*` и через JSON `modify.properties`.
| Свойство | Объекты | Inline-значение | | Свойство | Объекты | Inline-значение |
|----------|---------|-----------------| |----------|---------|-----------------|
@@ -20,31 +34,36 @@
| RegisterRecords | Document | `AccumulationRegister.XXX` | | RegisterRecords | Document | `AccumulationRegister.XXX` |
| BasedOn | Document, Catalog, BP, Task | `Document.XXX` | | BasedOn | Document, Catalog, BP, Task | `Document.XXX` |
| InputByString | Catalog, ChartOf*, Task | `StandardAttribute.Description` | | InputByString | Catalog, ChartOf*, Task | `StandardAttribute.Description` |
| DataLockFields | Catalog, Document, регистры и др. | `Организация` (короткое имя реквизита → полный путь) |
| RegisteredDocuments | DocumentJournal | `Document.XXX` |
### add-owner / add-registerRecord / add-basedOn ### add-owner / add-registerRecord / add-basedOn / add-registeredDocument
Полное имя метаданных `MetaType.Name`: Полное имя метаданных `MetaType.Name`:
```powershell ```powershell
-Operation add-owner -Value "Catalog.Контрагенты ;; Catalog.Организации" -Operation add-owner -Value "Catalog.Контрагенты ;; Catalog.Организации"
-Operation add-registerRecord -Value "AccumulationRegister.ОстаткиТоваров" -Operation add-registerRecord -Value "AccumulationRegister.ОстаткиТоваров"
-Operation add-basedOn -Value "Document.ЗаказКлиента" -Operation add-basedOn -Value "Document.ЗаказКлиента"
-Operation add-registeredDocument -Value "Document.РасходныйОрдер"
``` ```
### add-inputByString ### add-inputByString / add-dataLockField
Пути полей (префикс `MetaType.Name.` добавляется автоматически): Пути полей (короткое имя реквизита разворачивается в полный путь автоматически):
```powershell ```powershell
-Operation add-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code" -Operation add-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code"
-Operation add-dataLockField -Value "Организация ;; Контрагент"
``` ```
### remove-owner / remove-registerRecord / remove-basedOn / remove-inputByString ### remove-owner / remove-registerRecord / remove-basedOn / remove-inputByString / remove-dataLockField / remove-registeredDocument
```powershell ```powershell
-Operation remove-owner -Value "Catalog.Контрагенты" -Operation remove-owner -Value "Catalog.Контрагенты"
-Operation remove-inputByString -Value "Catalog.МойСпр.StandardAttribute.Code" -Operation remove-inputByString -Value "Catalog.МойСпр.StandardAttribute.Code"
-Operation remove-dataLockField -Value "Организация"
``` ```
### set-owners / set-registerRecords / set-basedOn / set-inputByString ### set-owners / set-registerRecords / set-basedOn / set-inputByString / set-dataLockFields / set-registeredDocuments
Заменяют **весь список** (в отличие от add/remove): Заменяют **весь список** (в отличие от add/remove):
```powershell ```powershell
+682 -12
View File
@@ -1,4 +1,4 @@
# meta-edit v1.9 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts) # meta-edit v1.22 — Edit existing 1C metadata object XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[string]$DefinitionFile, [string]$DefinitionFile,
@@ -12,14 +12,17 @@ param(
"add-attribute", "add-ts", "add-dimension", "add-resource", "add-attribute", "add-ts", "add-dimension", "add-resource",
"add-enumValue", "add-column", "add-form", "add-template", "add-command", "add-enumValue", "add-column", "add-form", "add-template", "add-command",
"add-owner", "add-registerRecord", "add-basedOn", "add-inputByString", "add-owner", "add-registerRecord", "add-basedOn", "add-inputByString",
"add-dataLockField", "add-registeredDocument", "add-predefined",
"remove-attribute", "remove-ts", "remove-dimension", "remove-resource", "remove-attribute", "remove-ts", "remove-dimension", "remove-resource",
"remove-enumValue", "remove-column", "remove-form", "remove-template", "remove-command", "remove-enumValue", "remove-column", "remove-form", "remove-template", "remove-command",
"remove-owner", "remove-registerRecord", "remove-basedOn", "remove-inputByString", "remove-owner", "remove-registerRecord", "remove-basedOn", "remove-inputByString",
"remove-dataLockField", "remove-registeredDocument",
"add-ts-attribute", "remove-ts-attribute", "modify-ts-attribute", "modify-ts", "add-ts-attribute", "remove-ts-attribute", "modify-ts-attribute", "modify-ts",
"modify-attribute", "modify-dimension", "modify-resource", "modify-attribute", "modify-dimension", "modify-resource",
"modify-enumValue", "modify-column", "modify-enumValue", "modify-column",
"modify-property", "modify-property",
"set-owners", "set-registerRecords", "set-basedOn", "set-inputByString" "set-owners", "set-registerRecords", "set-basedOn", "set-inputByString",
"set-dataLockFields", "set-registeredDocuments"
)] )]
[string]$Operation, [string]$Operation,
[string]$Value, [string]$Value,
@@ -78,7 +81,7 @@ $script:validEnumValues = @{
"Posting" = @("Allow","Deny") "Posting" = @("Allow","Deny")
"RealTimePosting" = @("Allow","Deny") "RealTimePosting" = @("Allow","Deny")
"EditType" = @("InDialog","InList","BothWays") "EditType" = @("InDialog","InList","BothWays")
"HierarchyType" = @("HierarchyFoldersAndItems","HierarchyItemsOnly") "HierarchyType" = @("HierarchyFoldersAndItems","HierarchyOfItems")
"CodeType" = @("String","Number") "CodeType" = @("String","Number")
"CodeAllowedLength" = @("Variable","Fixed") "CodeAllowedLength" = @("Variable","Fixed")
"NumberType" = @("String","Number") "NumberType" = @("String","Number")
@@ -167,6 +170,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null 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) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -203,10 +216,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} 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 $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { 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 $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -607,6 +623,8 @@ function Import-Fragment([string]$xmlString) {
xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"
xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"
xmlns:app="http://v8.1c.ru/8.2/managed-application/core"
xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"
xmlns:xs="http://www.w3.org/2001/XMLSchema">$xmlString</_W> xmlns:xs="http://www.w3.org/2001/XMLSchema">$xmlString</_W>
"@ "@
$frag = New-Object System.Xml.XmlDocument $frag = New-Object System.Xml.XmlDocument
@@ -890,20 +908,33 @@ $script:reservedAttrNames = @{
"Account"="Счет"; "ValueType"="ТипЗначения"; "ActionPeriodIsBasic"="ПериодДействияБазовый" "Account"="Счет"; "ValueType"="ТипЗначения"; "ActionPeriodIsBasic"="ПериодДействияБазовый"
} }
# Стандартные реквизиты по типу объекта (ключи из reservedAttrNames). Имя реквизита, совпадающее
# с ними (англ. ИЛИ рус.), платформа не позволит — жёсткий отказ. Контексты вне карты → предупреждение.
$script:reservedByContext = @{
"catalog" = @("Ref","DeletionMark","Predefined","PredefinedDataName","Code","Description","Owner","Parent","IsFolder")
"document" = @("Ref","DeletionMark","Date","Number","Posted")
}
function Build-AttributeFragment { function Build-AttributeFragment {
param($parsed, [string]$context, [string]$indent) param($parsed, [string]$context, [string]$indent)
if (-not $context) { $context = Get-AttributeContext } if (-not $context) { $context = Get-AttributeContext }
# Check reserved attribute names # Check reserved attribute names (типозависимо: catalog/document — жёсткий отказ; прочее — предупреждение)
$attrName = $parsed.name $attrName = $parsed.name
if ($script:reservedAttrNames.ContainsKey($attrName)) { $ctxReserved = $script:reservedByContext[$context]
if ($ctxReserved) {
foreach ($en in $ctxReserved) {
$ru = $script:reservedAttrNames[$en]
if (($attrName -ieq $en) -or ($ru -and $attrName -ieq $ru)) {
Write-Error "Имя реквизита '$attrName' зарезервировано стандартным реквизитом ($en/$ru) объекта '$context'. Выберите другое имя."
exit 1
}
}
} elseif ($context -notin @("tabular", "processor-tabular") -and
($script:reservedAttrNames.ContainsKey($attrName) -or ($script:reservedAttrNames.Values -contains $attrName))) {
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C." Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C."
} }
$ruValues = $script:reservedAttrNames.Values
if ($ruValues -contains $attrName) {
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name (Russian). This may cause errors when loading into 1C."
}
$uuid = New-Guid-String $uuid = New-Guid-String
$sb = New-Object System.Text.StringBuilder $sb = New-Object System.Text.StringBuilder
@@ -1475,6 +1506,8 @@ function Convert-InlineToDefinition([string]$operation, [string]$value) {
"registerRecord" = "RegisterRecords"; "registerRecords" = "RegisterRecords" "registerRecord" = "RegisterRecords"; "registerRecords" = "RegisterRecords"
"basedOn" = "BasedOn" "basedOn" = "BasedOn"
"inputByString" = "InputByString" "inputByString" = "InputByString"
"dataLockField" = "DataLockFields"; "dataLockFields" = "DataLockFields"
"registeredDocument" = "RegisteredDocuments"; "registeredDocuments" = "RegisteredDocuments"
} }
if ($complexTargetMap.ContainsKey($target)) { if ($complexTargetMap.ContainsKey($target)) {
@@ -1499,6 +1532,16 @@ function Convert-InlineToDefinition([string]$operation, [string]$value) {
return $def return $def
} }
# Предопределённые (Ext/Predefined.xml) — отдельный файл; строим { <op>: { predefined: [...] } }.
if ($target -eq 'predefined') {
$items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
$inner = New-Object PSCustomObject
$inner | Add-Member -NotePropertyName 'predefined' -NotePropertyValue $items
$def = New-Object PSCustomObject
$def | Add-Member -NotePropertyName $op -NotePropertyValue $inner
return $def
}
# TS attribute operations: dot notation "TSName.AttrDef" # TS attribute operations: dot notation "TSName.AttrDef"
if ($target -eq "ts-attribute") { if ($target -eq "ts-attribute") {
$items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) $items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
@@ -1741,6 +1784,10 @@ function Process-Add($addDef) {
$addDef.PSObject.Properties | ForEach-Object { $addDef.PSObject.Properties | ForEach-Object {
$rawKey = $_.Name $rawKey = $_.Name
$items = $_.Value $items = $_.Value
if ($rawKey -in @('predefined','предопределенные','предопределённые')) {
Add-PredefinedItems $items
return
}
$childType = Resolve-ChildTypeKey $rawKey $childType = Resolve-ChildTypeKey $rawKey
if (-not $childType) { if (-not $childType) {
@@ -1961,8 +2008,19 @@ function Modify-Properties($propsDef) {
} }
if (-not $propEl) { if (-not $propEl) {
Warn "Property '$propName' not found in Properties" # create-if-missing: известное свойство создаём (порядок 1С терпит — append); неизвестное → ошибка (опечатка)
return if ($script:knownObjectProps -notcontains $propName) {
Write-Error "modify-property: неизвестное свойство '$propName' — нет такого свойства объекта (опечатка?)"
exit 1
}
$newNodes = Import-Fragment "<$propName/>"
if ($newNodes.Count -gt 0) {
Insert-PropertyInOrder $script:propertiesEl $newNodes[0] $null $propName
$propEl = $newNodes[0]
} else {
Warn "Property '$propName': could not create element"
return
}
} }
# Complex property: Owners, RegisterRecords, BasedOn, InputByString # Complex property: Owners, RegisterRecords, BasedOn, InputByString
@@ -1983,6 +2041,32 @@ function Modify-Properties($propsDef) {
$valueStr = if ($propValue) { "true" } else { "false" } $valueStr = if ($propValue) { "true" } else { "false" }
} }
# Structural value-type property (корневой <Type> у Константы, ПВХ) —
# перестроить дескриптор типа через Build-ValueTypeXml (не расплющивать в скаляр)
if ($propName -ceq "Type") {
$typeIndent = Get-ChildIndent $script:propertiesEl
$newTypeXml = Build-ValueTypeXml $typeIndent $valueStr
$newTypeNodes = Import-Fragment $newTypeXml
if ($newTypeNodes.Count -gt 0) {
# ReplaceChild сохраняет whitespace до/после узла на месте (без склейки отступов)
$script:propertiesEl.ReplaceChild($newTypeNodes[0], $propEl) | Out-Null
Info "Modified property: Type = $valueStr"
$script:modifyCount++
}
return
}
# Guard: не расплющивать структурное свойство (с дочерними узлами) в скалярный текст —
# это молча повредит XML. Завершаем ошибкой ДО записи файла.
$hasChildElements = $false
foreach ($ch in $propEl.ChildNodes) {
if ($ch.NodeType -eq 'Element') { $hasChildElements = $true; break }
}
if ($hasChildElements) {
Write-Error "modify-property: свойство '$propName' структурное (содержит дочерние узлы) — установка скалярного текста повредит XML; не поддерживается"
exit 1
}
$propEl.InnerText = $valueStr $propEl.InnerText = $valueStr
Info "Modified property: $propName = $valueStr" Info "Modified property: $propName = $valueStr"
$script:modifyCount++ $script:modifyCount++
@@ -2216,6 +2300,56 @@ function Modify-ChildElements($modifyDef, [string]$childType) {
Info "Changed synonym of $xmlTag '$elemName': $changeValue" Info "Changed synonym of $xmlTag '$elemName': $changeValue"
$script:modifyCount++ $script:modifyCount++
} }
"Format" {
if (Set-AttrPropertyElement $propsEl "Format" (Build-MLTextXml (Get-ChildIndent $propsEl) "Format" "$changeValue")) {
Info "Set $xmlTag '$elemName'.Format"; $script:modifyCount++
}
}
"EditFormat" {
if (Set-AttrPropertyElement $propsEl "EditFormat" (Build-MLTextXml (Get-ChildIndent $propsEl) "EditFormat" "$changeValue")) {
Info "Set $xmlTag '$elemName'.EditFormat"; $script:modifyCount++
}
}
"ToolTip" {
if (Set-AttrPropertyElement $propsEl "ToolTip" (Build-MLTextXml (Get-ChildIndent $propsEl) "ToolTip" "$changeValue")) {
Info "Set $xmlTag '$elemName'.ToolTip"; $script:modifyCount++
}
}
"ChoiceForm" {
if (Set-AttrPropertyElement $propsEl "ChoiceForm" "<ChoiceForm>$(Esc-Xml "$changeValue")</ChoiceForm>") {
Info "Set $xmlTag '$elemName'.ChoiceForm"; $script:modifyCount++
}
}
"MinValue" {
if (Set-AttrPropertyElement $propsEl "MinValue" (Build-MinMaxValueXml "MinValue" $changeValue)) {
Info "Set $xmlTag '$elemName'.MinValue"; $script:modifyCount++
}
}
"MaxValue" {
if (Set-AttrPropertyElement $propsEl "MaxValue" (Build-MinMaxValueXml "MaxValue" $changeValue)) {
Info "Set $xmlTag '$elemName'.MaxValue"; $script:modifyCount++
}
}
"LinkByType" {
if (Set-AttrPropertyElement $propsEl "LinkByType" (Build-LinkByTypeXml (Get-ChildIndent $propsEl) $changeValue)) {
Info "Set $xmlTag '$elemName'.LinkByType"; $script:modifyCount++
}
}
"ChoiceParameterLinks" {
if (Set-AttrPropertyElement $propsEl "ChoiceParameterLinks" (Build-ChoiceParameterLinksXml (Get-ChildIndent $propsEl) $changeValue)) {
Info "Set $xmlTag '$elemName'.ChoiceParameterLinks"; $script:modifyCount++
}
}
"ChoiceParameters" {
if (Set-AttrPropertyElement $propsEl "ChoiceParameters" (Build-ChoiceParametersXml (Get-ChildIndent $propsEl) $changeValue)) {
Info "Set $xmlTag '$elemName'.ChoiceParameters"; $script:modifyCount++
}
}
"FillValue" {
if (Set-AttrPropertyElement $propsEl "FillValue" (Build-FillValueExplicitXml (Get-AttrTypeStrFromXml $propsEl) $changeValue)) {
Info "Set $xmlTag '$elemName'.FillValue"; $script:modifyCount++
}
}
default { default {
# Scalar property change (Indexing, FillChecking, Use, etc.) # Scalar property change (Indexing, FillChecking, Use, etc.)
$scalarEl = $null $scalarEl = $null
@@ -2235,7 +2369,23 @@ function Modify-ChildElements($modifyDef, [string]$childType) {
Info "Modified $xmlTag '$elemName'.$changeProp = $valueStr" Info "Modified $xmlTag '$elemName'.$changeProp = $valueStr"
$script:modifyCount++ $script:modifyCount++
} else { } else {
Warn "$xmlTag '$elemName': property '$changeProp' not found" # create-if-missing: известное свойство создаём в позиции; неизвестное → ошибка (опечатка)
if ($script:knownChildProps -notcontains $changeProp) {
Write-Error "modify: неизвестное свойство '$changeProp' у $xmlTag '$elemName' (опечатка?)"
exit 1
}
$valueStr = "$changeValue"
if ($changeValue -is [bool]) {
$valueStr = if ($changeValue) { "true" } else { "false" }
} else {
$valueStr = Normalize-EnumValue $changeProp $valueStr
}
$newNodes = Import-Fragment "<$changeProp>$(Esc-Xml $valueStr)</$changeProp>"
if ($newNodes.Count -gt 0) {
Insert-PropertyInOrder $propsEl $newNodes[0] $script:attrPropOrder $changeProp
Info "Created $xmlTag '$elemName'.$changeProp = $valueStr"
$script:modifyCount++
}
} }
} }
} }
@@ -2271,6 +2421,433 @@ $script:complexPropertyMap = @{
"RegisterRecords" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' } "RegisterRecords" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' }
"BasedOn" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' } "BasedOn" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' }
"InputByString" = @{ tag = "xr:Field"; attr = $null } "InputByString" = @{ tag = "xr:Field"; attr = $null }
"DataLockFields" = @{ tag = "xr:Field"; attr = $null; expand = $true }
"RegisteredDocuments" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' }
}
# Известные свойства объекта (union по корпусу acc+erp 8.3.24) — allowlist для modify-property.
# Известное отсутствующее свойство create-if-missing создаётся; неизвестное (опечатка) → ошибка.
$script:knownObjectProps = @(
'ActionPeriod','ActionPeriodUse','Addressing','AutoOrderByCode','Autonumbering','AuxiliaryChoiceForm',
'AuxiliaryFolderChoiceForm','AuxiliaryFolderForm','AuxiliaryForm','AuxiliaryListForm','AuxiliaryObjectForm',
'AuxiliaryRecordForm','AuxiliarySettingsForm','BaseCalculationTypes','BasePeriod','BasedOn',
'CharacteristicExtValues','Characteristics','ChartOfAccounts','ChartOfCalculationTypes','CheckUnique',
'ChoiceDataGetModeOnInputByString','ChoiceFoldersAndItems','ChoiceForm','ChoiceHistoryOnInput','ChoiceMode',
'ChoiceParameterLinks','ChoiceParameters','CodeAllowedLength','CodeLength','CodeMask','CodeSeries','CodeType',
'Comment','Correspondence','CreateOnInput','CreateTaskInPrivilegedMode','CurrentPerformer','DataHistory',
'DataLockControlMode','DataLockFields','DefaultChoiceForm','DefaultFolderChoiceForm','DefaultFolderForm',
'DefaultForm','DefaultListForm','DefaultObjectForm','DefaultPresentation','DefaultRecordForm','DefaultSettingsForm',
'DefaultVariantForm','DependenceOnCalculationTypes','DescriptionLength','DistributedInfoBase','EditFormat',
'EditType','EnableTotalsSliceFirst','EnableTotalsSliceLast','EnableTotalsSplitting',
'ExecuteAfterWriteDataHistoryVersionProcessing','Explanation','ExtDimensionTypes','ExtendedEdit',
'ExtendedListPresentation','ExtendedObjectPresentation','ExtendedPresentation','ExtendedRecordPresentation',
'FillChecking','FoldersOnTop','Format','FullTextSearch','FullTextSearchOnInputByString','Hierarchical',
'HierarchyType','IncludeConfigurationExtensions','IncludeHelpInContents','InformationRegisterPeriodicity',
'InputByString','LevelCount','LimitLevelCount','LinkByType','ListPresentation','MainAddressingAttribute',
'MainDataCompositionSchema','MainFilterOnPeriod','MarkNegatives','Mask','MaxExtDimensionCount','MaxValue',
'MinValue','MultiLine','Name','NumberAllowedLength','NumberLength','NumberPeriodicity','NumberType','Numerator',
'ObjectPresentation','OrderLength','Owners','PasswordMode','PeriodAdjustmentLength','Periodicity',
'PostInPrivilegedMode','Posting','PredefinedDataUpdate','QuickChoice','RealTimePosting','RecordPresentation',
'RegisterRecords','RegisterRecordsDeletion','RegisterRecordsWritingOnPost','RegisterType','RegisteredDocuments',
'Schedule','ScheduleDate','ScheduleValue','SearchStringModeOnInputByString','SequenceFilling','SettingsStorage',
'StandardAttributes','StandardTabularSections','SubordinationUse','Synonym','Task','TaskNumberAutoPrefix',
'ToolTip','Type','UnpostInPrivilegedMode','UpdateDataHistoryImmediatelyAfterWrite','UseStandardCommands',
'VariantsStorage','WriteMode'
)
# Известные свойства дочерних элементов (union Attribute/Dimension/Resource по корпусу) — allowlist default-ветки
# modify-attribute/-dimension/-resource.
$script:knownChildProps = @(
'AccountingFlag','Balance','BaseDimension','ChoiceFoldersAndItems','ChoiceForm','ChoiceHistoryOnInput',
'ChoiceParameterLinks','ChoiceParameters','Comment','CreateOnInput','DataHistory','DenyIncompleteValues',
'DocumentMap','EditFormat','ExtDimensionAccountingFlag','ExtendedEdit','FillChecking','FillFromFillingValue',
'FillValue','Format','FullTextSearch','Indexing','LinkByType','MainFilter','MarkNegatives','Mask','Master',
'MaxValue','MinValue','MultiLine','Name','PasswordMode','QuickChoice','RegisterRecordsMap','ScheduleLink',
'Synonym','ToolTip','Type','Use','UseInTotals'
)
# Канонический порядок свойств реквизита (последовательность Build-AttributeFragment) — для вставки в позицию.
# Порядок 1С терпит (cert), но держим канонический для консистентности с meta-compile.
$script:attrPropOrder = @(
'Name','Synonym','Comment','Type','PasswordMode','Format','EditFormat','ToolTip','MarkNegatives','Mask',
'MultiLine','ExtendedEdit','MinValue','MaxValue','FillFromFillingValue','FillValue','FillChecking',
'ChoiceFoldersAndItems','ChoiceParameterLinks','ChoiceParameters','QuickChoice','CreateOnInput','ChoiceForm',
'LinkByType','ChoiceHistoryOnInput','Use','Indexing','FullTextSearch','DataHistory'
)
# Вставить новый элемент свойства в Properties в канонической позиции (по orderArray); если свойства нет в
# orderArray (или orderArray пуст) — append. Порядок 1С терпит, канонический — для консистентности/снапшотов.
function Insert-PropertyInOrder($propsEl, $newNode, $orderArray, $propName) {
$childIndent = "$(Get-ChildIndent $propsEl)"
$refNode = $null
$idx = if ($orderArray) { [array]::IndexOf($orderArray, $propName) } else { -1 }
if ($idx -ge 0) {
foreach ($ch in $propsEl.ChildNodes) {
if ($ch.NodeType -eq 'Element') {
$ci = [array]::IndexOf($orderArray, $ch.LocalName)
if ($ci -gt $idx) { $refNode = $ch; break }
}
}
}
Insert-BeforeElement $propsEl $newNode $refNode $childIndent
}
# Заменить существующий элемент свойства реквизита новым фрагментом (по образцу ветки type),
# либо создать в канонической позиции, если его нет. Возвращает $true при успехе.
function Set-AttrPropertyElement($propsEl, $propName, $fragmentXml) {
$newNodes = Import-Fragment $fragmentXml
if ($newNodes.Count -eq 0) { return $false }
$existing = $null
foreach ($ch in $propsEl.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $propName) { $existing = $ch; break }
}
if ($existing) {
# InsertBefore+RemoveChild сохраняет ведущий/хвостовой whitespace позиции existing
# (InsertAfter+Remove-NodeWithWhitespace склеил бы: удаляет ведущий ws как отдельный узел).
$propsEl.InsertBefore($newNodes[0], $existing) | Out-Null
$propsEl.RemoveChild($existing) | Out-Null
} else {
Insert-PropertyInOrder $propsEl $newNodes[0] $script:attrPropOrder $propName
}
return $true
}
# MinValue/MaxValue — типизированное значение (порт Emit-MinMaxValue): nil / xs:string / xs:decimal.
function Build-MinMaxValueXml([string]$tag, $val) {
if ($null -eq $val -or "$val" -eq '') { return "<$tag xsi:nil=`"true`"/>" }
$t = if ($val -is [string]) { 'xs:string' } else { 'xs:decimal' }
return "<$tag xsi:type=`"$t`">$(Esc-Xml "$val")</$tag>"
}
# --- Порт из meta-compile: развёртка путей данных + связи выбора / тип по ссылке (structural modify) ---
# Свойство из dict/PSCustomObject по списку синонимов (первый найденный, иначе $null).
function Get-ChElProp($obj, [string[]]$names) {
if ($null -eq $obj) { return $null }
foreach ($n in $names) {
if ($obj -is [System.Collections.IDictionary]) { if ($obj.Contains($n)) { return $obj[$n] } }
elseif ($obj.PSObject -and $obj.PSObject.Properties[$n]) { return $obj.PSObject.Properties[$n].Value }
}
return $null
}
# Стандартный реквизит рус/англ → английский (для Catalog/Document); использует существующие reserved-карты.
function Resolve-StdAttrEn([string]$name) {
$ctx = switch ("$script:objType") { 'Catalog' { 'catalog' } 'Document' { 'document' } default { $null } }
if (-not $ctx) { return $null }
$stdSet = $script:reservedByContext[$ctx]
foreach ($en in $stdSet) {
$ru = $script:reservedAttrNames[$en]
if (($name -ieq $en) -or ($ru -and $name -ieq $ru)) { return $en }
}
return $null
}
# Прощающий ввод пути данных: короткое имя реквизита → полный путь объекта (порт Expand-DataPath).
function Expand-DataPath([string]$dp) {
if (-not $dp) { return $dp }
$s = "$dp"
if ($s -match '[:/]') { return $s }
if ($s -match '^-?\d+$') { return $s }
if ($s -match '^(StandardAttribute|Attribute)\.') { return "$($script:objType).$($script:objName).$s" }
if (-not $s.Contains('.')) {
$en = Resolve-StdAttrEn $s
if ($en) { return "$($script:objType).$($script:objName).StandardAttribute.$en" }
return "$($script:objType).$($script:objName).Attribute.$s"
}
return $s
}
# Shorthand "name=path" | "name=path:Clear|DontChange" → {name, dataPath, valueChange?}.
function ConvertFrom-ChLinkShorthand([string]$s) {
$eq = $s.IndexOf('=')
if ($eq -lt 0) { return @{ name = $s.Trim() } }
$o = @{ name = $s.Substring(0, $eq).Trim() }; $rest = $s.Substring($eq + 1).Trim()
if ($rest -match '^(.*):(?i:(Clear|DontChange|очистить|неизменять))$') { $o['dataPath'] = $matches[1].Trim(); $o['valueChange'] = $matches[2] }
else { $o['dataPath'] = $rest }
return $o
}
# LinkByType — {dataPath, linkItem?} (порт Emit-LinkByType). Строка → dataPath, linkItem=0.
function Build-LinkByTypeXml([string]$indent, $spec) {
if (-not $spec) { return "$indent<LinkByType/>" }
if ($spec -is [string]) { $dp = "$spec"; $li = 0 }
else {
$dp = "$(Get-ChElProp $spec @('dataPath','path','путь'))"
$liRaw = Get-ChElProp $spec @('linkItem','элементСвязи')
$li = if ($null -ne $liRaw) { $liRaw } else { 0 }
}
if (-not $dp) { return "$indent<LinkByType/>" }
$dp = Expand-DataPath $dp
$lines = @(
"$indent<LinkByType>"
"$indent`t<xr:DataPath>$(Esc-Xml "$dp")</xr:DataPath>"
"$indent`t<xr:LinkItem>$li</xr:LinkItem>"
"$indent</LinkByType>"
)
return $lines -join "`r`n"
}
# ChoiceParameterLinks — [{name, dataPath, valueChange?}] (порт Emit-ChoiceParameterLinks). valueChange дефолт Clear.
function Build-ChoiceParameterLinksXml([string]$indent, $cpl) {
if (-not $cpl -or @($cpl).Count -eq 0) { return "$indent<ChoiceParameterLinks/>" }
$sb = New-Object System.Text.StringBuilder
$sb.Append("$indent<ChoiceParameterLinks>") | Out-Null
foreach ($lk in @($cpl)) {
if ($lk -is [string]) { $lk = ConvertFrom-ChLinkShorthand $lk }
$name = Get-ChElProp $lk @('name','имя')
$dp = Expand-DataPath (Get-ChElProp $lk @('dataPath','path','путь'))
$vcRaw = Get-ChElProp $lk @('valueChange','режимИзменения')
$vc = 'Clear'
if ($vcRaw) {
$vc = switch -Regex ("$vcRaw".ToLower()) {
'^(clear|очистить|очистка)$' { 'Clear'; break }
'^(dontchange|неизменять|неменять|нет)$' { 'DontChange'; break }
default { "$vcRaw" }
}
}
$sb.Append("`r`n$indent`t<xr:Link>") | Out-Null
$sb.Append("`r`n$indent`t`t<xr:Name>$(Esc-Xml "$name")</xr:Name>") | Out-Null
$sb.Append("`r`n$indent`t`t<xr:DataPath xsi:type=`"xs:string`">$(Esc-Xml "$dp")</xr:DataPath>") | Out-Null
$sb.Append("`r`n$indent`t`t<xr:ValueChange>$vc</xr:ValueChange>") | Out-Null
$sb.Append("`r`n$indent`t</xr:Link>") | Out-Null
}
$sb.Append("`r`n$indent</ChoiceParameterLinks>") | Out-Null
return $sb.ToString()
}
# --- Порт из meta-compile: значения параметров выбора (ChoiceParameters) ---
$script:fillRefRoots = @{
'перечисление'='Enum'; 'справочник'='Catalog'; 'документ'='Document';
'плансчетов'='ChartOfAccounts'; 'планвидовхарактеристик'='ChartOfCharacteristicTypes';
'планвидоврасчета'='ChartOfCalculationTypes'; 'планвидоврасчёта'='ChartOfCalculationTypes';
'планобмена'='ExchangePlan'; 'бизнеспроцесс'='BusinessProcess'; 'задача'='Task';
'enum'='Enum'; 'catalog'='Catalog'; 'document'='Document'; 'chartofaccounts'='ChartOfAccounts';
'chartofcharacteristictypes'='ChartOfCharacteristicTypes'; 'chartofcalculationtypes'='ChartOfCalculationTypes';
'exchangeplan'='ExchangePlan'; 'businessprocess'='BusinessProcess'; 'task'='Task'
}
$script:fillEmptyRefWords = @('emptyref','пустаяссылка')
$script:fillEnumValWords = @('enumvalue','значениеперечисления')
$script:accountTypeValues = @('Active','Passive','ActivePassive')
$script:fillRefKindRoot = @{
'catalogref'='Catalog'; 'documentref'='Document'; 'enumref'='Enum';
'chartofaccountsref'='ChartOfAccounts'; 'chartofcharacteristictypesref'='ChartOfCharacteristicTypes';
'chartofcalculationtypesref'='ChartOfCalculationTypes'; 'exchangeplanref'='ExchangePlan';
'businessprocessref'='BusinessProcess'; 'taskref'='Task'
}
function ConvertTo-ChScalar([string]$s) {
$t = "$s".Trim()
if ($t -match '^(?i:true|истина)$') { return $true }
if ($t -match '^(?i:false|ложь)$') { return $false }
if ($t -match '^-?\d+$') { return [int]$t }
if ($t -match '^-?\d+\.\d+$') { return [double]::Parse($t, [System.Globalization.CultureInfo]::InvariantCulture) }
return $t
}
function Format-FillNum($n) {
if ($n -is [double] -or $n -is [decimal]) { return $n.ToString([System.Globalization.CultureInfo]::InvariantCulture) }
return "$n"
}
function Normalize-FillRef([string]$s) {
if ([string]::IsNullOrEmpty($s)) { return $null }
if ($s -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\.[0-9a-fA-F-]+$') { return $s }
$parts = $s -split '\.'
if ($parts.Count -lt 2) { return $null }
$root = $script:fillRefRoots[$parts[0].ToLower()]
if (-not $root) { return $null }
$typeName = $parts[1]
if ($root -eq 'Enum') {
if ($parts.Count -eq 2) { return $null }
if ($parts.Count -eq 3) {
if ($script:fillEmptyRefWords -contains $parts[2].ToLower()) { return "Enum.$typeName.EmptyRef" }
return "Enum.$typeName.EnumValue.$($parts[2])"
}
$member = $parts[2]
if ($script:fillEnumValWords -contains $member.ToLower()) { $rest = $parts[3..($parts.Count-1)] -join '.' }
else { $rest = $parts[2..($parts.Count-1)] -join '.' }
return "Enum.$typeName.EnumValue.$rest"
}
$tail = @($parts[1..($parts.Count-1)])
for ($i = 0; $i -lt $tail.Count; $i++) {
if ($script:fillEmptyRefWords -contains $tail[$i].ToLower()) { $tail[$i] = 'EmptyRef' }
}
return "$root." + ($tail -join '.')
}
function Expand-ChoiceRefValue([string]$value, [string]$typeStr) {
if (-not $typeStr) { return $null }
$t = Resolve-TypeStr $typeStr
$root = $null; $tn = $null
if ($t -match '^(\w+Ref)\.(.+)$') { $root = $script:fillRefKindRoot[$Matches[1].ToLower()]; $tn = $Matches[2] }
elseif ($t -match '^([^.]+)\.(.+)$') { $root = $script:fillRefRoots[$Matches[1].ToLower()]; $tn = $Matches[2] }
if (-not $root) { return $null }
if ($script:fillEmptyRefWords -contains "$value".ToLower()) { return "$root.$tn.EmptyRef" }
if ($root -eq 'Enum') { return "Enum.$tn.EnumValue.$value" }
return "$root.$tn.$value"
}
function Normalize-ChoiceValue($value) {
if ($value -is [bool]) { return @{ XsiType='xs:boolean'; Text=$(if ($value) { 'true' } else { 'false' }) } }
if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) {
return @{ XsiType='xs:decimal'; Text=(Format-FillNum $value) }
}
$s = "$value"
if ($s -eq '') { return @{ XsiType='xs:string'; Text='' } }
if ($s -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') { return @{ XsiType='xs:dateTime'; Text=$s } }
$ref = Normalize-FillRef $s
if ($ref) { return @{ XsiType='xr:DesignTimeRef'; Text=$ref } }
if ($script:accountTypeValues -contains $s) { return @{ XsiType='ent:AccountType'; Text=$s } }
return @{ XsiType='xs:string'; Text=$s }
}
function Normalize-ChoiceValueT($value, [string]$typeStr) {
if ($typeStr -and ($value -is [string]) -and (-not "$value".Contains('.'))) {
$ex = Expand-ChoiceRefValue "$value" $typeStr
if ($ex) { return @{ XsiType='xr:DesignTimeRef'; Text=$ex } }
}
return Normalize-ChoiceValue $value
}
function ConvertFrom-ChParamShorthand([string]$s) {
$eq = $s.IndexOf('=')
if ($eq -lt 0) { return @{ name = $s.Trim() } }
$name = $s.Substring(0, $eq).Trim(); $rest = $s.Substring($eq + 1)
if ($rest -match ',') {
$vals = @(); foreach ($p in ($rest -split ',')) { $vals += ,(ConvertTo-ChScalar $p) }
return @{ name = $name; value = $vals }
}
return @{ name = $name; value = (ConvertTo-ChScalar $rest) }
}
# ChoiceParameters — [{name, type?, value?}] (порт Emit-ChoiceParameters). Значение на app:value (xsi:type=тип);
# массив → v8:FixedArray с v8:Value; без value → nil. Требует xmlns:app в Import-Fragment.
function Build-ChoiceParametersXml([string]$indent, $cp) {
if (-not $cp -or @($cp).Count -eq 0) { return "$indent<ChoiceParameters/>" }
$sb = New-Object System.Text.StringBuilder
$sb.Append("$indent<ChoiceParameters>") | Out-Null
foreach ($item in @($cp)) {
if ($item -is [string]) { $item = ConvertFrom-ChParamShorthand $item }
$name = Get-ChElProp $item @('name','имя')
$ptype = Get-ChElProp $item @('type','тип')
$hasVal = $false; $val = $null
if ($item -is [System.Collections.IDictionary]) {
if ($item.Contains('value')) { $hasVal = $true; $val = $item['value'] }
elseif ($item.Contains('значение')) { $hasVal = $true; $val = $item['значение'] }
} elseif ($item.PSObject) {
if ($item.PSObject.Properties['value']) { $hasVal = $true; $val = $item.PSObject.Properties['value'].Value }
elseif ($item.PSObject.Properties['значение']) { $hasVal = $true; $val = $item.PSObject.Properties['значение'].Value }
}
$valIsArray = ($val -is [System.Array]) -or ($val -is [System.Collections.IList] -and $val -isnot [string])
$sb.Append("`r`n$indent`t<app:item name=`"$(Esc-Xml "$name")`">") | Out-Null
if (-not $hasVal) {
$sb.Append("`r`n$indent`t`t<app:value xsi:nil=`"true`"/>") | Out-Null
} elseif ($valIsArray) {
$sb.Append("`r`n$indent`t`t<app:value xsi:type=`"v8:FixedArray`">") | Out-Null
foreach ($v in $val) {
$norm = Normalize-ChoiceValueT $v $ptype
if ([string]::IsNullOrEmpty($norm.Text)) { $sb.Append("`r`n$indent`t`t`t<v8:Value xsi:type=`"$($norm.XsiType)`"/>") | Out-Null }
else { $sb.Append("`r`n$indent`t`t`t<v8:Value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</v8:Value>") | Out-Null }
}
$sb.Append("`r`n$indent`t`t</app:value>") | Out-Null
} else {
$norm = Normalize-ChoiceValueT $val $ptype
if ([string]::IsNullOrEmpty($norm.Text)) { $sb.Append("`r`n$indent`t`t<app:value xsi:type=`"$($norm.XsiType)`"/>") | Out-Null }
else { $sb.Append("`r`n$indent`t`t<app:value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</app:value>") | Out-Null }
}
$sb.Append("`r`n$indent`t</app:item>") | Out-Null
}
$sb.Append("`r`n$indent</ChoiceParameters>") | Out-Null
return $sb.ToString()
}
# --- Порт из meta-compile: явное значение заполнения (FillValue) ---
$script:fillBoolTrue = @('true','истина','да')
$script:fillBoolFalse = @('false','ложь','нет')
function Esc-XmlText([string]$s) { return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;') }
function Get-FillTypeCategory([string]$typeStr) {
if (-not $typeStr) { return 'String' }
if ($typeStr -match '\+') { return 'Other' }
$t = Resolve-TypeStr $typeStr
if ($t -match '^Boolean$') { return 'Boolean' }
if ($t -match '^String(\(|$)') { return 'String' }
if ($t -match '^Number(\(|$)') { return 'Number' }
if ($t -match '^(Date|DateTime)$') { return 'Date' }
return 'Other'
}
function Expand-FillShortRef([string]$s, [string]$typeStr) {
if (-not $typeStr) { return $null }
if ($typeStr -match '\+') { return $null }
$t = Resolve-TypeStr $typeStr
if ($t -notmatch '^(\w+Ref)\.(.+)$') { return $null }
$root = $script:fillRefKindRoot[$Matches[1].ToLower()]
if (-not $root) { return $null }
$typeName = $Matches[2]
if ($script:fillEmptyRefWords -contains $s.ToLower()) { return "$root.$typeName.EmptyRef" }
if ($root -eq 'Enum') { return "Enum.$typeName.EnumValue.$s" }
return "$root.$typeName.$s"
}
function Resolve-FillValueSpec([string]$s, [string]$typeStr) {
$cat = Get-FillTypeCategory $typeStr
if ($s -eq '') { return @{ XsiType='xs:string'; Text='' } }
if ($cat -eq 'String') { return @{ XsiType='xs:string'; Text=$s } }
if ($cat -eq 'Boolean' -or ($script:fillBoolTrue -contains $s.ToLower()) -or ($script:fillBoolFalse -contains $s.ToLower())) {
if ($script:fillBoolTrue -contains $s.ToLower()) { return @{ XsiType='xs:boolean'; Text='true' } }
if ($script:fillBoolFalse -contains $s.ToLower()) { return @{ XsiType='xs:boolean'; Text='false' } }
}
if ($cat -eq 'Number') { return @{ XsiType='xs:decimal'; Text=$s } }
if ($cat -eq 'Date' -or $s -match '^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?$') {
if ($s -match '^\d{4}-\d{2}-\d{2}$') { $s = "${s}T00:00:00" }
return @{ XsiType='xs:dateTime'; Text=$s }
}
$ref = Normalize-FillRef $s
if ($ref) { return @{ XsiType='xr:DesignTimeRef'; Text=$ref } }
$short = Expand-FillShortRef $s $typeStr
if ($short) { return @{ XsiType='xr:DesignTimeRef'; Text=$short } }
return @{ XsiType='xs:string'; Text=$s }
}
# Извлечь тип реквизита из XML (<Type>/<v8:Type>) → DSL-typeStr для категоризации FillValue.
function Get-AttrTypeStrFromXml($propsEl) {
$typeEl = $null
foreach ($ch in $propsEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'Type') { $typeEl = $ch; break } }
if (-not $typeEl) { return "" }
$mapped = @()
foreach ($ch in $typeEl.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'Type') {
$t = $ch.InnerText.Trim()
$colon = $t.IndexOf(':'); if ($colon -ge 0) { $t = $t.Substring($colon + 1) }
switch -Regex ($t) {
'^string$' { $mapped += 'String'; break }
'^decimal$' { $mapped += 'Number'; break }
'^boolean$' { $mapped += 'Boolean'; break }
'^dateTime$' { $mapped += 'Date'; break }
default { $mapped += $t }
}
}
}
if ($mapped.Count -eq 0) { return "" }
if ($mapped.Count -gt 1) { return ($mapped -join ' + ') }
return $mapped[0]
}
# FillValue — явное значение (порт Emit-FillValue, ветка hasSpec). Маркеры {nil}/{emptyRef}; иначе по типу.
function Build-FillValueExplicitXml([string]$typeStr, $spec) {
if ($null -eq $spec) { return "<FillValue xsi:nil=`"true`"/>" }
if ($spec -is [bool]) { return "<FillValue xsi:type=`"xs:boolean`">$(if ($spec) { 'true' } else { 'false' })</FillValue>" }
if ($spec -is [int] -or $spec -is [long] -or $spec -is [double] -or $spec -is [decimal]) { return "<FillValue xsi:type=`"xs:decimal`">$(Format-FillNum $spec)</FillValue>" }
if ((Get-ChElProp $spec @('nil')) -eq $true) { return "<FillValue xsi:nil=`"true`"/>" }
if ((Get-ChElProp $spec @('emptyRef','пустаяссылка')) -eq $true) { return "<FillValue xsi:type=`"xr:DesignTimeRef`"/>" }
$r = Resolve-FillValueSpec "$spec" $typeStr
if ($r.Text -eq '' -and $r.XsiType -eq 'xs:string') { return "<FillValue xsi:type=`"xs:string`"/>" }
return "<FillValue xsi:type=`"$($r.XsiType)`">$(Esc-XmlText $r.Text)</FillValue>"
} }
function Find-PropertyElement([string]$propName) { function Find-PropertyElement([string]$propName) {
@@ -2295,6 +2872,7 @@ function Get-ComplexPropertyValues([System.Xml.XmlElement]$propEl) {
function Add-ComplexPropertyItem([string]$propertyName, [string[]]$values) { function Add-ComplexPropertyItem([string]$propertyName, [string[]]$values) {
$mapEntry = $script:complexPropertyMap[$propertyName] $mapEntry = $script:complexPropertyMap[$propertyName]
if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return } if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return }
if ($mapEntry.expand) { $values = @($values | ForEach-Object { Expand-DataPath "$_" }) }
$propEl = Find-PropertyElement $propertyName $propEl = Find-PropertyElement $propertyName
if (-not $propEl) { if (-not $propEl) {
@@ -2342,6 +2920,8 @@ function Add-ComplexPropertyItem([string]$propertyName, [string[]]$values) {
} }
function Remove-ComplexPropertyItem([string]$propertyName, [string[]]$values) { function Remove-ComplexPropertyItem([string]$propertyName, [string[]]$values) {
$mapEntry = $script:complexPropertyMap[$propertyName]
if ($mapEntry -and $mapEntry.expand) { $values = @($values | ForEach-Object { Expand-DataPath "$_" }) }
$propEl = Find-PropertyElement $propertyName $propEl = Find-PropertyElement $propertyName
if (-not $propEl) { if (-not $propEl) {
Warn "Property element '$propertyName' not found in Properties" Warn "Property element '$propertyName' not found in Properties"
@@ -2379,6 +2959,7 @@ function Remove-ComplexPropertyItem([string]$propertyName, [string[]]$values) {
function Set-ComplexProperty([string]$propertyName, [string[]]$values) { function Set-ComplexProperty([string]$propertyName, [string[]]$values) {
$mapEntry = $script:complexPropertyMap[$propertyName] $mapEntry = $script:complexPropertyMap[$propertyName]
if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return } if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return }
if ($mapEntry.expand) { $values = @($values | ForEach-Object { Expand-DataPath "$_" }) }
$propEl = Find-PropertyElement $propertyName $propEl = Find-PropertyElement $propertyName
if (-not $propEl) { if (-not $propEl) {
@@ -2428,6 +3009,95 @@ function Set-ComplexProperty([string]$propertyName, [string[]]$values) {
# Section 13: Main processing # Section 13: Main processing
# ============================================================ # ============================================================
# ============================================================
# Predefined data (Ext/Predefined.xml) — add предопределённых (Catalog/ChartOfCharacteristicTypes).
# Существующие <Item id=GUID> сохраняются побайтово (текстовый append), новые получают свежий GUID —
# инвариант «не менять id существующей сущности».
# ============================================================
$script:predefXsiTypeByObj = @{
'Catalog' = 'CatalogPredefinedItems'
'ChartOfCharacteristicTypes' = 'PlanOfCharacteristicKindPredefinedItems'
}
function Get-PredefinedPath {
$objDir = Join-Path (Split-Path $resolvedPath) $script:objName
return (Join-Path (Join-Path $objDir "Ext") "Predefined.xml")
}
function Get-ObjectCodeType {
foreach ($ch in $script:propertiesEl.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'CodeType') { return $ch.InnerText.Trim() }
}
return 'String'
}
# Элемент DSL: строка "(Код) Имя [Наименование]" ЛИБО объект {name,code,description,isFolder,childItems}.
function Resolve-PredefItem($val) {
if ($val -is [string]) {
$s = "$val"; $descRaw = $null; $hasDesc = $false
if ($s -match '\[(.*)\]') { $descRaw = $Matches[1]; $hasDesc = $true; $s = $s -replace '\s*\[.*\]', '' }
$m = [regex]::Match($s.Trim(), '^\s*(?:\(([^)]*)\)\s*)?(\S+)\s*$')
$name = $m.Groups[2].Value
$code = if ($m.Groups[1].Success) { $m.Groups[1].Value } else { '' }
$desc = if ($hasDesc) { $descRaw } else { Split-CamelCase $name }
return @{ name = $name; code = $code; desc = $desc; isFolder = $false; children = @() }
}
$gv = { param($o, [string[]]$keys) foreach ($k in $keys) { if ($o.PSObject.Properties[$k]) { return $o.$k } } return $null }
$name = "$(& $gv $val @('name','имя'))"
$codeV = & $gv $val @('code','код'); $code = if ($null -ne $codeV) { "$codeV" } else { '' }
$hasDesc = $val.PSObject.Properties['description'] -or $val.PSObject.Properties['наименование']
$descV = & $gv $val @('description','наименование')
$desc = if ($hasDesc) { "$descV" } else { Split-CamelCase $name }
$isFolder = ((& $gv $val @('isFolder','группа')) -eq $true)
$subs = & $gv $val @('childItems','подчиненные')
return @{ name = $name; code = $code; desc = $desc; isFolder = $isFolder; children = @(if ($subs) { @($subs) } else { @() }) }
}
function Build-PredefItemXml([string]$indent, $val, [string]$codeType) {
$r = Resolve-PredefItem $val
$sb = New-Object System.Text.StringBuilder
[void]$sb.Append("$indent<Item id=`"$(New-Guid-String)`">`r`n")
[void]$sb.Append("$indent`t<Name>$(Esc-XmlText $r.name)</Name>`r`n")
if (-not $r.code) { [void]$sb.Append("$indent`t<Code/>`r`n") }
elseif ($codeType -eq 'Number') { [void]$sb.Append("$indent`t<Code xsi:type=`"xs:decimal`">$(Esc-XmlText $r.code)</Code>`r`n") }
else { [void]$sb.Append("$indent`t<Code>$(Esc-XmlText $r.code)</Code>`r`n") }
if ($r.desc -eq '') { [void]$sb.Append("$indent`t<Description/>`r`n") }
else { [void]$sb.Append("$indent`t<Description>$(Esc-XmlText $r.desc)</Description>`r`n") }
[void]$sb.Append("$indent`t<IsFolder>$(if ($r.isFolder) { 'true' } else { 'false' })</IsFolder>`r`n")
if ($r.children.Count -gt 0) {
[void]$sb.Append("$indent`t<ChildItems>`r`n")
foreach ($c in $r.children) { [void]$sb.Append((Build-PredefItemXml "$indent`t`t" $c $codeType)) }
[void]$sb.Append("$indent`t</ChildItems>`r`n")
}
[void]$sb.Append("$indent</Item>`r`n")
return $sb.ToString()
}
function Add-PredefinedItems($items) {
$xsiType = $script:predefXsiTypeByObj[$script:objType]
if (-not $xsiType) { Write-Error "add-predefined: тип объекта '$($script:objType)' не поддержан (только Catalog, ChartOfCharacteristicTypes)"; exit 1 }
$codeType = Get-ObjectCodeType
$version = $script:xmlDoc.DocumentElement.GetAttribute("version")
$path = Get-PredefinedPath
$itemsXml = ""
foreach ($it in @($items)) { $itemsXml += (Build-PredefItemXml "`t" $it $codeType) }
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
if (Test-Path $path) {
$text = [System.IO.File]::ReadAllText($path, $utf8Bom)
$text = $text.Replace("</PredefinedData>", "$itemsXml</PredefinedData>")
} else {
$extDir = Split-Path $path
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
$hdr = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" 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`" xsi:type=`"$xsiType`" version=`"$version`">`r`n"
$text = "$hdr$itemsXml</PredefinedData>`r`n"
}
[System.IO.File]::WriteAllText($path, $text, $utf8Bom)
$n = @($items).Count
Info "Added $n predefined item(s) → $path"
$script:addCount += $n
}
# --- Inline mode conversion --- # --- Inline mode conversion ---
if ($Operation) { if ($Operation) {
$def = Convert-InlineToDefinition $Operation $Value $def = Convert-InlineToDefinition $Operation $Value

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