Compare commits

...

178 Commits

Author SHA1 Message Date
Nick Shirokov 7fa279c354 feat(skd-edit): clear-conditionalAppearance + multiline patch-query (доки)
- Новая операция clear-conditionalAppearance в стиле clear-selection/
  order/filter. Закрывает потребность "заменить набор правил оформления"
  через clear + re-add.
- patch-query: многострочные подстроки уже работали (string.Replace
  корректно обрабатывает \n). Зафиксировано в SKILL.md.
- add-total: shorthand-шаблон с тремя случаями (Func, Func(expr),
  identity-выражение) — после fix Bug 6 поведение нужно явно объяснить.
- Косметика: убрана утечка XML-внутренностей в комментарии примера
  set-field-role @period.
- Пример patch-query @once заменён на более типовой случай уникальной
  подстроки (КАК ВТ_СтароеИмя вместо ЛЕВОЕ СОЕДИНЕНИЕ).

Регресс: 33/33 PS, 33/33 PY, 33/33 платформенный verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:39:24 +03:00
Nick Shirokov 28a2a34c84 fix(skd-edit): add-total identity expression для не-аггрегатных функций
Раньше "DataPath: X" всегда заворачивалось в X(DataPath). Если X не
аггрегатная функция (например, имя другого ресурса или сам DataPath),
получалось некорректное выражение типа Проверка(Проверка).

Зеркалю логику из skd-compile: whitelist аггрегатных функций
(Сумма, Количество, Минимум, Максимум, Среднее + EN-варианты).
Для остального — identity (использовать funcPart как есть).

Сообщение [OK] теперь показывает фактически записанный expression.

Регресс: 32/32 PS, 32/32 PY, 32/32 платформенный verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:16:27 +03:00
Nick Shirokov f0f1e88aaa feat(skd-edit): patch-query @once — assert ровно одно вхождение
Защищает от случайных замен в комментариях/совпадениях имён:

  "ЛЕВОЕ СОЕДИНЕНИЕ => ВНУТРЕННЕЕ СОЕДИНЕНИЕ @once"
  # fail, если в запросе 0 или 2+ вхождений

Без флага default — replace-all (как раньше, обратная совместимость).

При успехе сообщение содержит фактическое число вхождений
"(N occurrence(s))", помогает заметить неожиданную множественность
без явного @once.

Регресс: 31/31 PS, 31/31 PY, 31/31 платформенный verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:00:55 +03:00
Nick Shirokov e7cbf306a0 feat(skd-edit): availableValue — список с replace-семантикой и в add-parameter
- Единый list-синтаксис: availableValue=v1[: p1], v2[: p2], ...
  Элементы через запятую, представление после двоеточия.
- Запятые/двоеточия внутри значений и представлений — в одинарных кавычках:
  availableValue=Окр1: 'руб., коп.', Окр1000: руб.
- add-parameter теперь принимает availableValue= и создаёт начальный список
  в одном вызове (раньше требовался последующий modify-parameter).
- modify-parameter availableValue=... ЗАМЕНЯЕТ весь список (раньше
  append). Согласуется с остальными modify-* для одиночных свойств.
- SKILL.md: добавлен shorthand-шаблон для modify-parameter,
  расширен для add-parameter [availableValue=список].

Существующие тесты мигрированы со старого ;;-batch на новый list-синтаксис.
Снапшоты сохранились (тесты стартовали с пустого списка — semantics
совпадает для greenfield).

Регресс: 29/29 PS, 29/29 PY, 29/29 платформенный verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:37:56 +03:00
Nick Shirokov 610720334b feat(skd-info): kv-параметры роли в детализации поля
В -Mode fields -Name <field> к сводке Role добавляются не-bool
параметры роли (balanceGroupName, balanceType, parentDimension,
accountTypeExpression и т.д.) в формате name=value.

Bool-флаги (@balance, @dimension, ...) отображаются как раньше.
False-значения по-прежнему скрыты.

Регресс: 6/6 PS, 6/6 PY (существующие snapshots не задеты).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:54:03 +03:00
Nick Shirokov 5090deb5bc feat(skd-edit): set-field-role — управление ролями поля
Новая операция: полная замена <role>-блока поля dataSet.

- Shorthand: "<dataPath> [@флаги] [kv=значение]"
- Флаги (зеркало skd-compile): @balance, @dimension, @account, @period,
  @required, @autoOrder, @ignoreNullValues
- KV: balanceGroupName, balanceType, parentDimension, accountTypeExpression,
  orderType, expression, periodNumber, periodType
- Пустой spec (только dataPath) — снимает роль целиком
- Поддерживает пакетный режим

Закрывает потребность временного toggle off/on роли при отладке
(было: ручной Edit XML), а также корректировку balance/dimension
после add-total.

Регресс: 27/27 PS, 27/27 PY, 27/27 платформенный verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:53:55 +03:00
Nick Shirokov 8b0bcf0194 feat(skd-edit): флаги @hidden и @always для параметров
- @hidden — скрывает параметр от пользовательских настроек
  (useRestriction=true + availableAsField=false). Для констант-параметров.
- @always — параметр всегда подставляется в запрос (use=Always).
  Используется самостоятельно для видимых обязательных параметров.
- Композируются: @hidden @always одной строкой даёт типовой паттерн
  "скрытая константа всегда применяется".
- Поддержка в add-parameter и modify-parameter, идемпотентны.

Регресс: 25/25 PS, 25/25 PY, 25/25 платформенный verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:14:19 +03:00
Nick Shirokov 529a5cacae feat(skd-edit): modify-structure + фиксы set-structure/parameter/patch-query
- modify-structure: новая операция, меняет groupItems группы по @name=,
  сохраняя Selection/order/filter/conditionalAppearance (Bug 1)
- set-structure: shorthand поддерживает запятую для нескольких полей
  в одном уровне группировки (Bug 2)
- set-structure: @name= с обрамляющими кавычками (двойными/одинарными)
  снимает их при записи в <dcsset:name> (Bug 3)
- add-parameter: ссылочные типы (CatalogRef, ChartOfAccountsRef, …)
  пишут <value xsi:type="dcscor:DesignTimeValue">, не xs:string (Bug 4a)
- modify-parameter: namespace-aware lookup существующих свойств
  — обновляет inplace, не плодит дубли (Bug 4b)
- modify-parameter value=…: пересборка <value> с корректным xsi:type
  из <valueType> (попутно лечит ранее битый XML)
- patch-query: батч ;;-сегментов триммится по краям (Bug 5)
- skd-compile: симметричный фикс ссылочных типов в emit_value

Регресс: 23/23 PS, 23/23 PY (skd-edit), 21/21 PS+PY (skd-compile),
23/23 платформенный verify-snapshots.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:40:49 +03:00
Nick Shirokov 8b0f55f1cc feat(form-validate): silent-skip числовых и UUID-DataPath в Check 5
В реальных выгрузках ERP/БП встречаются непрозрачные платформенные
DataPath, которые невозможно проверить из одного Form.xml:
- bare numeric ("10", "1000003") — внутренние индексы платформы
- "N/M:<uuid>" — ссылка на метаданные по UUID

Раньше Check 5 ругался на них "attribute not found". Теперь такие
пути пропускаются без счёта в paths checked и без ошибки.

Реалистичные пользовательские опечатки (кириллица в имени атрибута)
продолжают ловиться обычной проверкой attrMap.

Добавлен тест-кейс datapath-opaque-refs, версия v1.5 → v1.6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:40:27 +03:00
Nick Shirokov 54cbc69a59 feat(form-validate): резолв Items.<Table>.CurrentData.* и ~<Attr>.* в DataPath
В Check 5 раньше брался первый сегмент DataPath и искался в attrMap,
из-за чего ложно ругались реальные формы ERP/БП с путями вида
Items.<TableName>.CurrentData.<Field> (подвалы, инфо-панели) и
~<DynamicListAttr>.<Field> (текущая строка списка).

Теперь:
- ведущий ~ стрипается перед разбором сегментов;
- для Items.<Table>.CurrentData.* находим элемент-таблицу по name,
  берём её <DataPath> (атрибут DynamicList/TableSection) и проверяем
  его в attrMap. Если таблицы нет — Error; если третий сегмент не
  CurrentData — Warn.

Добавлен тест-кейс datapath-currentdata, версия скриптов v1.4 → v1.5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:38:56 +03:00
Nick Shirokov ac3047cf55 docs(readme): ссылка на гайд регрессионного тестирования
Добавлена строка в таблицу навыков и в перечень docs/ для нового
docs/web-test-regression-guide.md (был забыт при первичном коммите
регресс-гайда).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 20:41:33 +03:00
Nick Shirokov 5da154adea Merge feature/web-test-runner into dev
web-test regression runner: M5-pre синтетика + M6 автономный стенд +
M7 testInfo/contexts/testlevel-хуки + M8 per-context lifecycle +
Allure-форматтер с auto-suite/severity + _allure/categories.json +
пользовательский гайд регресса (docs/web-test-regression-guide.md) +
skill-инструкция regress.md.
2026-05-13 20:28:39 +03:00
Nick Shirokov f4748d76af docs(web-test): пользовательский гайд регресса + skill-инструкция regress.md
- docs/web-test-regression-guide.md — пользовательские сценарии работы
  с моделью для покрытия прикладного решения регрессом (русский, по
  аналогии с web-test-recording-guide.md): структура tests/<app-name>/,
  диалоги с моделью, пример организации покрытия, отчёты Allure +
  categories.json.
- .claude/skills/web-test/regress.md — инструкция модели по написанию
  регрессионного набора: разведка (метаданные + живой проход через exec),
  layout по фичам, готовые шаблоны (CRUD/document/DCS/multi-user/repro),
  severity, anti-patterns, failure triage, _allure/ конвенция.
- SKILL.md — указатель на regress.md в конце файла (рядом с recording).
- docs/web-test-runner-spec.md → upload/ (был внутренним планом
  разработки, не пользовательской документацией).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 20:25:44 +03:00
Nick Shirokov b992cd11c5 feat(web-test): _allure/ конвенция + categories.json для триажа падений
run.mjs:
- syncAllureExtras(testDir, reportDir) копирует все файлы из
  <testDir>/_allure/ в reportDir перед генерацией отчёта. Underscore
  в имени параллелен _hooks.mjs (инфра, не тест) — discovery его
  пропускает.
- Вызов после writeAllure при --format=allure.

tests/web-test/_allure/categories.json — 7 правил классификации падений
по нашему 1С-домену:
  1. License pool exhausted (1C) — известный multi-context flake.
  2. 1C application error (modal) — exception modal через fetchErrorStack.
  3. Section panel icon-only — деградация состояния стенда.
  4. Navigation lookup miss — navigateSection/openCommand/navigateLink/switchTab.
  5. Element not found — clickElement/fillField/selectValue/closeForm/fillTableRow/deleteTableRow.
  6. Test timeout — Timeout (Nms) от раннера.
  7. Assertion failure — наши createAssertions + 1С-specific (formHasField/tableHasRow/noErrors).

spec §9: раздел «Доп. файлы Allure через <testDir>/_allure/» с таблицей
поддерживаемых типов (categories.json / environment.properties /
executor.json) и минимальным примером.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:53:09 +03:00
Nick Shirokov fc76407877 feat(web-test): auto-suite + severity-резолвер для Allure
run.mjs:
- buildSeverityIndex(config) — валидация config.severity (inverted map
  «уровень → [теги]») при загрузке: ключи только из blocker|critical|
  normal|minor|trivial, теги не дублируются между bucket'ами,
  defaultSeverity тоже валидируется. fail-fast через die.
- resolveSeverity(t, severityIndex):
  1. mod.severity если задан и валидный — выигрывает.
  2. max-rank среди тегов (стандартные имена severity или маппинг).
  3. config.defaultSeverity или 'normal'.
  Rank: blocker(5) > critical(4) > normal(3) > minor(2) > trivial(1).
  Max-wins инвариантен к порядку тегов.
- writeAllure: добавлены labels suite (= dirname(t.file) или 'root') +
  severity. Тег `tag` остался как раньше.
- testResult пробрасывает t.severity (для passed/failed веток).
- SEVERITY_RANK/LEVELS объявлены в модульной шапке (top-level await на
  cmdTest начинается до конца тела модуля, TDZ-аккуратность).

webtest.config.mjs: severity policy для нашего сьюта (smoke +
multi-context → critical, recording → minor, defaultSeverity = normal).

spec.md §7: раздел про severity-policy в конфиге с валидацией.
spec.md §9: «Авто-эмиссия label-ов» — tag/suite/severity + правила резолва.

Регресс 19/19 ✓ (9m 7.6s). Распределение по уровням после исправления
'record' → 'recording' в маппинге: 13 critical / 5 normal / 1 minor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:37:58 +03:00
Nick Shirokov a55195ab66 docs(web-test): §16.1 — вложенные каталоги (что работает, что нет)
Зафиксирована конвенция:
- Discovery рекурсивный, путь попадает в отчёт.
- Per-folder hooks/config/context-default НЕ поддерживаются (by design).
- Группировку в отчётах делать через tags, не через путь.
- Сортировка по полному пути (`warehouse/01-x` после `sales/02-y`) —
  для глобального порядка нужны 3-значные префиксы или теги-фазы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:33:10 +03:00
Nick Shirokov 1eff62de42 docs(web-test): полный sync спеки + contexts[] в testResult
spec.md v0.2 (последний sync 2026-05-13):

§1 CLI: добавлены --report-dir и `--` separator в таблицу флагов.
§1 «Режим выполнения»: убрана несуществующая «группировка по контексту»,
  описана реальная алфавитная модель + lazy ensureContext.
§2 пример multi-context: latin ID контекстов вместо кириллицы (clerk/manager)
  + showcase closeContext в финальном шаге.
§3 список API расширен: контексты (createContext/closeContext/setActive/
  listContexts/hasContext/getActiveContext), overlay-helpers (hideTitleSlide/
  hideImage/setHighlight/isHighlightMode), error-helpers (dismissPendingErrors/
  fetchErrorStack).
§6 пример _hooks.mjs: убран mock-навигация в beforeAll, добавлены примеры
  afterOpenContext/beforeCloseContext, afterEach показывает testResult.
§8 переписан раздел «Реализация в browser.mjs» (мульти-контекст уже live)
  + новая таблица режимов изоляции tab/window.
§9 JSON example: поле "context" → "contexts": [...] (массив).
§10: убрано упоминание несуществующего verbose-режима.
§13 «Параметризация»: убран статус «будущее», описана реальная семантика
  T6 (template name, param 2-м аргументом, testInfo.param).
§14 buildContext: переписан под done-состояние + scoped-вариант.
§16 каталог тест-кейсов: 13 → 19 файлов (multi-context, recording,
  errors-stack, tree-form, misc, hooks).
§17 дорожная карта: 10 → 18 пунктов, M4–M8 включены.

run.mjs:
- testResult получил поле contexts: [...names] во всех ветках
  (passed/failed/skipped/context-setup-failed). Раннер передаёт
  declaredContexts из единой точки до if(skip), чтобы skip-результаты
  тоже несли структурную информацию.

Регресс 19/19 ✓ (9m 8.7s) после --rebuild-stand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:11:51 +03:00
Nick Shirokov eb87be5c04 feat(web-test): M8 — per-context lifecycle (closeContext + afterOpenContext/beforeCloseContext)
browser.mjs:
- + closeContext(name): logout slot + close page (tab) или context (window),
  удаление из реестра. Throw если name неактивен (рулило: nicht den aktiven
  closen, recorder always attached к active → invariant простой).
- _logoutSlot(slot, waitMs) — извлечён из disconnect, переиспользуется в
  closeContext.

run.mjs:
- ensureContext() после createContext вызывает hooks.afterOpenContext(ctx, name, spec).
- wrapCloseContextHook() оборачивает ctx.closeContext (и каждую scoped-обёртку)
  чтобы перед browser.closeContext fir'ить hooks.beforeCloseContext.
- Финальный teardown в finally: для всех живых контекстов кроме первого
  (survivor) — beforeCloseContext + closeContext; для survivor только хук,
  его закрывает disconnect().

_hooks.mjs v0.5:
- afterOpenContext инжектит persistent DOM-badge с displayName в правый
  верхний угол page — в записанном видео всегда видно, какой контекст.
- beforeCloseContext counter-only.
- _state расширен полями afterOpenContext / beforeCloseContext.

15-multi-context-handover.test.mjs:
- +2 шага: closeContext('b') после handover, попытка closeContext(active)
  ловится throw'ом с проверкой message.

00-hooks.test.mjs:
- +1 ассерт: afterOpenContext >= 1 (default уже создан), beforeCloseContext === 0
  в теле первого теста.

spec §6:
- Раздел «Контекстный уровень» (afterOpenContext / beforeCloseContext + правила closeContext).
- ASCII-диаграмма порядка хуков обновлена с per-context lifecycle.

Регресс 19/19 ✓ (9m 16.8s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:07:45 +03:00
Nick Shirokov 43ed9ba142 feat(web-test): M7.5 — title slide в beforeEach для --record
_hooks.mjs v0.4: beforeEach под условием ctx.isRecording() показывает
title slide с testInfo.name + displayName первичного контекста как
subtitle, ждёт 1.5с через ctx.wait() и убирает.

В обычном регрессе (без --record) — ветка скипается, overhead ноль.
Под --record: 01-navigation 12.1s → 13.9s (+1.8с на слайд).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:45:14 +03:00
Nick Shirokov 588382cec1 feat(web-test): M7.4 — testlevel-хуки + 00-hooks индикатор
_hooks.mjs v0.3: добавлены beforeAll/afterAll/beforeEach/afterEach
(counter-only) и shared `_state` (счётчики + events log).

tests/web-test/00-hooks.test.mjs (новый, 4 шага, 0s) — индикатор
порядка вызовов: проверяет beforeAll===1, beforeEach для текущего
теста, доступность ctx.testInfo, afterEach < beforeEach.

Multi-context хуки оставлены one-shot. Разведка beforeAll:
navigateSection не нужен, 1С после входа уже на дефолтной секции.

Регресс 19/19 ✓ (9m 12.7s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:35:20 +03:00
Nick Shirokov e0197683e1 feat(web-test): M7.1+M7.2 — ctx.testInfo + проброс custom-полей контекстов
- ctx.testInfo (name/file/filePath/tags/timeout/attempt/maxAttempts/param/contexts/primaryContext)
  выставляется перед каждой попыткой, доступен в beforeEach/test/afterEach
- ctx.testResult (status/duration/attempts/error/steps) доступен в afterEach
- run.mjs:411 spread полного contextSpec (был whitelist {url, isolation});
  CLI --url override сохраняет custom-поля через merge
- webtest.config.mjs: displayName для a/b
- spec §3 — подраздел «Метаданные теста», §6 — availability testInfo/testResult,
  §7 — рекомендация латинский ID + кириллический displayName
- Full regression 18/18 ✓ (9m 9.8s)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:44:07 +03:00
Nick Shirokov 96dad75b2f feat(web-test): M6-MVP follow-up — 13-misc setup + URL webtest-runner
13-misc.test.mjs: setup-шаг упрощён до `assert.ok(existsSync(epfPath))`.
EPF-сборку (epf-init → form-add → form-compile → epf-build) забрал
_hooks.mjs.prepare() — здесь только проверка артефакта с понятной
ошибкой при отсутствии: «запустите раннер с `-- --rebuild-epf`».

webtest.config.mjs: URL обоих контекстов переключён на
`/webtest-runner/ru_RU` — отдельная публикация автономного стенда,
не конфликтует с интерактивной разведкой через `/webtest` на 8081.
2026-05-12 20:25:54 +03:00
Nick Shirokov 5c734202b6 feat(web-test): M6-MVP — автономный стенд через _hooks.mjs
Новый tests/web-test/_hooks.mjs v0.2 с prepare()/cleanup().
prepare() поднимает изолированный стенд:
- Hash-locks `tests/skills/.cache/webtest-stand/{config,epf}.lock`
  на sha256 от build-steps и EPF_SPEC — автоматический skip
  пересборки при отсутствии изменений.
- Слои конфиг XML / БД / EPF пересобираются независимо. Триггер
  ручной — флаги `--rebuild-config`/`--reload-data`/`--rebuild-epf`/
  `--rebuild-stand` (через `-- ...` после CLI раннера).
- Smart Apache: web-stop+web-publish выполняются только когда
  пересоздаём БД (нужно освободить блокировку). Иначе probe-first:
  жив (200) → no-op; мёртв → publish + probeReady. На warm-старте
  prepare сводится к чтению локов и одному probe (~200ms).
- web-publish на собственном AppName `webtest-runner` :9191 — не
  пересекается с интерактивной публикацией `webtest`.
- Кросс-платформенно: env WEBTEST_HOOKS_RUNTIME=python переключает
  на зеркальные py-порты скиллов (для не-Windows стендов).

cleanup() пока stub — оставляем стенд поднятым между прогонами,
для full-shutdown ручной /web-stop или `-- --rebuild-stand`.

E2E-проверено: cold-start `--rebuild-stand` поднимает стенд за
~38s; warm-старт prepare = 0.0s; полный регресс 18/18 зелёный
за 9m 7.1s (включая оба multi-context-теста, которые исторически
флапали).
2026-05-12 20:25:47 +03:00
Nick Shirokov a92bce05fb feat(web-test): runner v1.11 — -- separator + spec §6.1
В CLI раннера всё после `--` собирается в массив hookArgs и
передаётся в инфра-хуки prepare/cleanup без интерпретации со
стороны раннера. Сигнатура расширена до { hookArgs, log, config }:
log — структурированный вывод раннера, config — разобранный
webtest.config.mjs. Шаблон «всё после `--` принадлежит вложенному
инструменту» — стандартная shell-конвенция (npm, cargo, pytest).

Спека §6 обновлена под новую сигнатуру, §6.1 закрепляет контракт
`--` ↔ hookArgs с примером. Help-строка раннера упоминает
разделитель.
2026-05-12 20:25:33 +03:00
Nick Shirokov b8ebbf6a6f feat(build-webtest-db): v0.2 — dual-mode CLI + module exports
Извлечены exports: getProjectInfo, resolveScript, execSkill,
replacePlaceholders, runSteps, platformLoadSteps, loadBuildSteps.
CLI-режим сохранён через import.meta.url-guard. Подготовка к
переиспользованию из tests/web-test/_hooks.mjs без дублирования
exec-логики и pipeline-шагов.
2026-05-12 20:25:25 +03:00
Nick Shirokov 43ba6ce16c feat(web-test): M5-pre #4b — 09-filter/unfilter-specific (multi-badge)
Раньше шаг был deferred с комментарием «требует список с видимой
filter-панелью». На самом деле существующая абстракция работает:
два advanced filterList на разных колонках Контрагентов создают
два badge'а в state.filters[], а unfilterList({field}) снимает
конкретный — оставляя остальные.

Новый шаг 09-filter/unfilter-specific (~14s):
- filterList('ООО', {field:'Наименование'}) + filterList('123', {field:'ИНН'})
  → state.filters = [{field:'Наименование',value:'ООО'}, {field:'ИНН',value:'123'}]
- unfilterList({field:'ИНН'}) → остался только Наименование badge
- unfilterList() → пусто

Старый комментарий «defer to filter-panel synthetic» удалён —
оказался устаревшим (видимо unfilterList({field}) уже умел работать
с advanced-filter badge'ами на синтетических списках).

timeout 09-filter поднят с 60000 → 120000ms (8 шагов теперь, +14s
для unfilter-specific).

Регресс: 16/18 зелёных. Два multi-context-теста (14/15) упали на
лицензионном пределе 1С — known environmental issue, не связано с
этим коммитом.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:01:00 +03:00
Nick Shirokov 51e37f9874 feat(web-test): M5-pre #4a — Менеджер (choiceHistoryOnInput=Auto) + selectValue/auto-history
Реквизит шапки ПриходнаяНакладная.Менеджер типа CatalogRef.Контрагенты
с дефолтным choiceHistoryOnInput=Auto. Существующий Контрагент в той же
шапке имеет DontUse, что даёт парный контраст для тестирования влияния
флага на selectValue.

Новый шаг 04-selectvalue/auto-history:
- selectValue('Менеджер', 'ООО Юг') → method='dropdown' (typeahead активен,
  префиксный поиск по Description находит «ООО Юг» в catalogue).
- Парный 04-selectvalue/direct-form (existing): selectValue('Контрагент',
  'Север') → method='form' (typeahead подавлен DontUse → форма выбора).

Тест покрывает существующее ветвление selectValue по флагу
choiceHistoryOnInput без engine-доработок. Истории на сервере писать
заранее не нужно: typeahead использует prefix-match по Description,
а не статистику истории.

Полный регресс **18/18 зелёный** (8m 47.3s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:56:00 +03:00
Nick Shirokov 62e864e474 feat(web-test): M5-pre #3 — textEdit:false поле + 03-fillfields/direct-edit-form
Расширение синтетики: реквизит Поставщик типа CatalogRef.Контрагенты
добавлен в шапку ПриходнаяНакладная. Элемент формы Поставщик скомпилирован
с textEdit:false (новый DSL ключ form-compile v1.21 из коммита 32bf9c1):
ручной ввод запрещён, селект-кнопки нет, выбор только через форму выбора
по pick-кнопке.

Новый шаг 03-fillfields/direct-edit-form (~7s) — fillFields на Поставщик
('ООО Юг') возвращает method:'form', минуя обычные paste/typeahead/dropdown
ветки. fillFields внутренне детектит textEdit:false и сразу идёт через
форму выбора (selectValue path).

Полный регресс **18/18 зелёный** (8m 40.6s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:11:46 +03:00
Nick Shirokov ddebd7b6df feat(web-test): M5-pre #2 — составной тип Источник + 03-fillfields/composite
Расширение синтетики: реквизит Источник составного типа
(CatalogRef.Контрагенты + CatalogRef.Номенклатура + CatalogRef.Организации)
добавлен в шапку ПриходнаяНакладная и в ТЧ Товары. meta-compile принимает
составной тип через строковый синтаксис `A + B + C` (см. SKILL.md:56) —
эмитит три `<v8:Type>` элемента с правильным `d5p1:` префиксом.

Элемент ТЧ-колонки переименован в ИсточникТЧ (path/title оставлены
оригинальные) — иначе form-compile генерирует одинаковые companion-имена
(`ИсточникКонтекстноеМеню`) для шапки и ТЧ, и платформа отказывает в
открытии формы документа: "К сожалению, возникла непредвиденная ошибка"
(server-side, без полезного stack). TODO в form-compile-bugs.md: учитывать
путь поля при генерации companion-имён, чтобы избежать конфликта.

Новый шаг 03-fillfields/composite (~25s) — покрывает selectValue с
параметром `{type}` на составном поле:
- Шапка: selectValue('Источник', 'ООО Север', {type:'Контрагенты'})
  → method:'form', type:'Контрагенты', выбор через каталог-форму.
- ТЧ: fillTableRow({Источник: {value:'Альфа', type:'Организации'}},
  {row:0}) → method:'form', type:'Организации' (quickChoice=true →
  без формы выбора, прямой dropdown).

fillFields на composite без type выбрасывает понятную ошибку
с инструкцией «specify the type: selectValue(...,{type:'ИмяТипа'})» —
поведение API стабильно.

timeout 03-fillfields поднят с 60000 → 120000ms (6 шагов суммарно
~63s, новый composite step добавляет ~25s).

Полный регресс **18/18 зелёный** (8m 28.7s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:51:41 +03:00
Nick Shirokov 3d16e35e80 feat(web-test): M5-pre #1 — ValueTree + ДеревоНоменклатуры + tree-form smoke
Расширение синтетики: новая обработка ДеревоНоменклатуры с реквизитом
формы Дерево типа ДеревоЗначений и колонками Номенклатура (ссылка,
read-only) + Цена (Number, editable). ПриСозданииНаСервере рекурсивно
обходит Справочник.Номенклатура и заполняет дерево, отражая иерархию
групп/элементов из справочника.

Обработка зарегистрирована в подсистеме Администрирование и в роли
Администратор (Use+View).

Новый smoke 16-tree-form.test.mjs (5 шагов, 17.1s) — покрывает
05-table/edit-form (fillTableRow method:'direct' на FormDataTree-колонке)
и 08-hierarchy/tree-edit (expand узла + правка Цены через index-row):
- setup: navigateLink('Обработка.ДеревоНоменклатуры'), таблица Дерево
- read-roots: 2 корневые группы (_kind:'group'), columns=Номенклатура,Цена
- expand: clickElement('Товары',{expand:true}) → 16 строк (1 + 15)
- tree-edit: fillTableRow({Цена:1500},{row:1}) → method:'direct',
  Цена становится '1 500,00' (с non-breaking space 1С)
- cleanup: closeForm

Гэп: fillTableRow с row-by-name ('Товар 01') ловит SyntaxError в JS
eval. Использую row-by-index (TODO в web-test-bugs).

Полный регресс **18/18 зелёный** (8m 9.8s) на порту 9191.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:03:28 +03:00
Nick Shirokov 56822c4533 test(web-test): switch webtest publication to port 9191
Чтобы не конфликтовать с интерактивной разработкой на основном
Apache (8081, занят сторонним проектом), регрессионный регресс
теперь использует отдельный httpd-процесс на порту 9191. Тот же
httpd запускает /web-publish webtest -Port 9191 -V8Path 8.3.24.

Один процесс Apache → собственный пул лицензий 1С. На 8081 другие
проекты — наши тесты их не блокируют и наоборот.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:03:15 +03:00
Nick Shirokov 32bf9c1a3f feat(form-compile): textEdit key for InputField (TextEdit=false)
v1.20 → v1.21 (ps1 + py).

Добавлен ключ DSL `textEdit` для элемента input. Эмитит
`<TextEdit>false</TextEdit>` после AutoMarkIncomplete (значение
true — дефолт платформы, не эмитируется). Закрывает блокер для
03-fillfields/direct-edit-form в синтетике web-test: поле с
запрещённым ручным вводом → выбор только через pick-кнопку/F4.

Snapshot-тест: tests/skills/cases/form-compile/text-edit-flag.json
(2 поля, проверяет наличие TextEdit только на втором). 30/30
form-compile зелёные обоих runtime'ов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 12:56:35 +03:00
Nick Shirokov c94f86a9cd test(web-test): M4.D2 — openFile EPF + security confirm
Новый 13-misc.test.mjs (3 шага, 11s) — покрытие openFile() для
внешних обработок с автоматической обработкой security confirmation.

- setup: автономный билд EPF (идемпотентный) через epf-init →
  form-add → form-compile (с текстовой декорацией) → epf-build.
  child_process.spawnSync для вызова PowerShell скриптов.
- openFile: проверки state.form, activeTab='Тест открытия',
  state.texts[] содержит декорацию с ожидаемым value,
  opened.attempt>=1, security confirm modal не пробивается.
- cleanup: closeForm + soft-проверка activeTab (между тестами в
  desktop могут оставаться формы от других тестов — не настаиваем
  на formCount=0).

Артефакты в test-tmp/13-openfile/ (.gitignore). Полный регресс
17/17 зелёный (8m 8s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:27:22 +03:00
Nick Shirokov 8b5fed98e0 test(web-test): M4.E — hierarchy + tree-grid (Номенклатура)
Новый 08-hierarchy.test.mjs (7 шагов, 24s) — покрывает группы и
tree-grid режима «Дерево» на форме списка Номенклатуры через UI
переключение viewMode. Без расширения синтетики.

- setup: явное переключение в «Иерархический список» через Ещё →
  Режим просмотра (viewMode сохраняется между сессиями и НЕ
  сбрасывается «Установить стандартные настройки»).
- read-groups (P1): readTable возвращает 2 группы (_kind=group).
- group-expand (P1): clickElement({expand:true}) развёртывает группу,
  внутри 15 элементов.
- switch-tree: «Ещё → Режим просмотра → Дерево» → viewMode='tree'.
- read-tree (P2): readTable.rows[]._tree (collapsed|expanded) — проверка
  только наличия поля (состояние сохраняется между сессиями).
- tree-expand (P1): defensive свёртка через {expand:false} если узел
  expanded, затем {expand:true} → kind='gridTreeNode' toggled=true,
  видны 15 элементов под Товарами.
- cleanup: восстановить иерархический список.

Замечание: clickElement({expand:true}) — только развернуть (no-op для
expanded), {expand:false} — только свернуть, {toggle:true} —
безусловно переключить.

05-table/direct-edit-form, edit-dblclick остаются deferred — нужен
документ с иерархической ТЧ. Полный регресс 16/16 зелёный (7m 53s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:43:31 +03:00
Nick Shirokov 9e677cfc61 test(web-test): M4.F — recording smoke (video + captions + TTS + overlays)
Новый 15-recording.test.mjs (5 шагов, 20.7s) — покрытие полного
публичного API recording.md.

- record + captions: startRecording → 2× showCaption → stopRecording.
  Проверки isRecording, duration/size, mp4 на диске, .captions.json,
  getCaptions с правильными text и time.
- narration: addNarration через Edge TTS (ru-RU-DmitryNeural), narrated
  mp4 больше исходного (добавлен аудио-трек).
- title-slide: showTitleSlide/hideTitleSlide — overlay fullscreen
  (w==innerWidth, h==innerHeight).
- image-overlay: showImage/hideImage с тестовой картинкой из screenshot.
- highlight: setHighlight toggles isHighlightMode, manual highlight на
  кнопке «Создать» создаёт overlay позиционированный на элементе.

Артефакты в test-tmp/recording-smoke/ (.gitignore), идемпотентный.
Полный регресс 15/15 зелёный (7m 27s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:20:13 +03:00
Nick Shirokov 211a4726d6 test(web-test): M4.C+D — drill-down + submenu-read
11-report/drill-down: dblclick по ячейке Номенклатуры сформированного
DCS-отчёта открывает форму элемента (DCS auto-drill). После Сформировать
ищется первая строка с заполненной номенклатурой, проверяется что после
clickElement({row,column},{dblclick:true}) form изменился и есть кнопка
«Записать».

02-crud/more-menu усилен под P2 submenu-read: добавлены явные проверки
clicked.kind='submenu', наличия типовых пунктов «Создать», «Изменить»,
«Расширенный поиск» (length>=5).

Покрыто 2 P2-кейса coverage matrix (11-report/drill-down,
02-crud/submenu-read). Полный регресс 14/14 зелёный (7m 1.6s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:58:28 +03:00
Nick Shirokov 91b39b758b test(web-test): M4.B+G — subordinate-nav + platform dialogs в 12-formstate
Расширены тесты getFormState: проверка ветвей navigation[] и
platformDialogs[] возвращаемой структуры.

- subordinate-nav: форма элемента Контрагент → state.navigation содержит
  «Основное» (active) и «Контактные лица» (подчинённый каталог).
- platform-dialogs: открытый через hamburger «О программе…» виден в
  state.platformDialogs[{type:'about'}].
- platform-dialog-close: closeForm закрывает платформенный диалог,
  массив становится пустым.

Покрыто 3 P2-кейса coverage matrix (12-formstate/subordinate-nav,
platform-dialogs, platform-dialog-close). Полный регресс 14/14 зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:30:51 +03:00
Nick Shirokov 4af69f1600 test(web-test): M4.A — validation messages + exception modal + error stack
10-validation.test.mjs (3 шага): Сообщить() → state.errors.messages,
ВызватьИсключение → onecError.errors.modal с автоматическим закрытием
fetchErrorStack.

14-errors-stack.test.mjs (3 шага): Path 1 OpenReport автоматически фетчит
стек для серверных исключений (entries[] содержит кадр ОбщиеФункции);
оставленная error modal через raw page.click закрывается closeForm;
платформенный диалог «О программе» виден в state.platformDialogs и
закрывается closeForm.

Покрыто 4 P2-кейса coverage matrix: 10-validation/messages,
10-validation/exception-modal, 14-errors/path1, 14-errors/dismiss-platform
+ бонус dismiss-modal. Открытие обработки ТестовыеОшибки через
navigateLink('Обработка.ТестовыеОшибки') — стандартные команды у
DataProcessor отключены.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:21:11 +03:00
Nick Shirokov 8c7c442705 feat(meta-compile): DSL choiceHistoryOnInput для аттрибутов
meta-compile v1.12 (ps1 + py): Parse-AttributeShorthand принимает поле
choiceHistoryOnInput в object-форме аттрибута, Emit-Attribute эмитит его
вместо хардкода Auto. Покрывает атрибуты Catalog/Document/TabularSection
(Emit-Attribute, единственная точка эмиссии в работе). Другие контексты
(register dimensions, resources, etc.) пока эмитят Auto — расширим
при необходимости.

build-webtest-config: реквизит Документ.ПриходнаяНакладная.Контрагент
получил choiceHistoryOnInput='DontUse'. Это убирает 1С-историю выбора
для поля и фиксит pre-existing flake 04-selectvalue/direct-form:
после 03 значение «ООО Север» оставалось в истории и selectValue
выбирал его через dropdown вместо ожидаемой формы выбора.

Live: полный регресс 12/12 впервые зелёный (5m 28s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:47:50 +03:00
Nick Shirokov c541d51f33 fix(web-test): resetState не закрывал form 0 + error screenshot снимался после reset
run.mjs:

1. resetState проверял `if (!state.form) break`. form === 0 (фоновая
   форма 1С, которую detectForm может вернуть) рассматривался как
   "форм нет" → cleanup прерывался, форма оставалась → следующий тест
   получал грязное состояние. Замена на `state.form == null` корректно
   различает null (desktop) и 0 (реальная фоновая форма).

2. Error screenshot в catch-блоке cmdTest снимался ПОСЛЕ resetState,
   который уже закрывал все формы → скрин показывал пустой рабочий
   стол вместо места падения. Перенёс снимок в начало catch (до
   teardown/afterEach/resetState).

Эффекты:
- 15-multi-context-handover теперь стабильно проходит в полном прогоне
  (раньше падал когда предыдущий тест оставлял form=0).
- 04-selectvalue/direct-form остался pre-existing flake (история
  выбора 1С после 03 — отдельная задача в синтетике).
- Скриншоты падения теперь показывают реальный UI на момент исключения.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:52:22 +03:00
Nick Shirokov a650325baf fix(web-test): убрать стуб showCaption/hideCaption в cmdTest
run.mjs v1.10: cmdTest больше не передаёт noRecord:true в buildContext.
Тестам доступен полный API browser.mjs (showCaption, hideCaption,
startRecording, stopRecording, addNarration).

Изначальный стуб с noRecord:true прятал showCaption/hideCaption тестов
вместе с recording-функциями. Это блокировало визуальные оверлеи в
мульти-контекстных тестах: a.showCaption() тихо превращался в no-op,
баннер никогда не отображался даже под --record.

Smart wait внутри showCaption и так гейтится на наличие recorder
(`if (recorder && ...)`), поэтому без --record тесты остаются быстрыми
(никаких 2-секундных пауз на каждый вызов).

startRecording/stopRecording/addNarration теперь тоже доступны тестам.
При попытке вызвать startRecording в момент активной runner-записи
browser.startRecording бросает "Already recording" — loud failure
лучше silent no-op.

Регресс: 15-multi-context-handover один проходит за 19.9s. Полный
прогон 10/12 (04 и 15 флапают независимо в последовательности).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:37:42 +03:00
Nick Shirokov 6c19846051 feat(web-test): T4.6 — гибридные режимы изоляции контекстов (tab default, window opt-in)
browser.mjs v1.12 + run.mjs v1.9: createContext принимает isolation параметр.
По умолчанию 'tab' — все контексты живут в одном launchPersistentContext, каждый
слот получает свою Page (вкладку). Преимущества: 1С extension грузится
надёжно (через --load-extension в persistent profile), один процесс Chromium,
дешёвая память. Cookies делятся между вкладками, но скоупятся по URL-path —
для модели «разные пользователи через разные vrd-публикации» это естественно
и достаточно.

isolation: 'window' (opt-in) — старый путь chromium.launch() + newContext():
полная изоляция cookies, отдельный BrowserContext (и окно) на каждый слот,
но extension может не подняться. Использовать когда нужна изоляция auth
внутри одного URL.

Смешивать режимы в одном прогоне нельзя — createContext бросает явную
ошибку (первый createContext устанавливает activeMode, остальные обязаны
совпадать).

Конфиг tests/web-test/webtest.config.mjs: добавлен комментарий с описанием
обоих режимов. По умолчанию tab — синтетика и наши smoke-тесты идут им.

Live: 11/12 в полном прогоне (default tab) + 3/3 sanity-check в window mode
(01-navigation + 14 + 15). Видеозапись из T4.5 работает в обоих режимах.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:34:44 +03:00
Nick Shirokov eef4f4bcea feat(web-test): T4.5 — мульти-контекстная запись видео
browser.mjs v1.11: recorder стал глобальным (не per-slot) — один ffmpeg,
один mp4 на тест с любым числом переключений контекста.

Frame state (lastFrameBuf/lastFrameTime/handler) переехал в поля recorder.
Добавлен recorder._attachPage(targetPage) — стопает старый CDP screencast,
заводит новый на нужной странице, route'ит фреймы в тот же ffmpeg pipe.

setActiveContext: при активной записи делает _flushFrames (замораживает
хвост уходящего окна), затем _attachPage(page) после _activateSlot. Видео
получается непрерывным с плавным сюжетом — пока активен a, видно a; пока
активен b, видно b.

_saveActiveSlot/_activateSlot больше не трогают recorder/lastCaptions/
lastRecordingDuration — recorder следует за активной страницей через
_attachPage, не через slot mirror.

disconnect: убрал leftover из T4.1, который пытался итерировать slot.recorder.

Live: 15-multi-context-handover с --record → 17.84s mp4, 446 кадров @ 25fps,
извлечённые кадры показывают переключение между окнами a (1920x1042) и
b (982x546). Полный регресс 11/12 (04-selectvalue — pre-existing flake).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:58:31 +03:00
Nick Shirokov 2c553fee98 feat(web-test): T4 — мульти-контекст BrowserContext
browser.mjs v1.10: createContext/setActiveContext/listContexts/getActiveContext/
hasContext. Несколько изолированных BrowserContext в одном Chromium-процессе через
chromium.launch() + newContext(). Module-level page/sessionPrefix/seanceId/recorder
зеркалят активный слот (атомарный своп через _saveActiveSlot/_activateSlot).
connect() оставлен для exec/run/start без изменений (launchPersistentContext).

run.mjs v1.8: ensureContext(name) + ленивое создание. Single-routing через
export const context = 'name'. Multi через export const contexts = ['a','b'] +
buildScopedContext(name) строит ctx.a/ctx.b — каждое действие префиксится
setActiveContext. Reset state после теста по всем активным контекстам.

Конфиг tests/web-test/webtest.config.mjs: два контекста a/b на одну webtest
публикацию (изолированные cookies через newContext).

Smoke-тесты:
- 14-multi-context-routing.test.mjs — single routing в b (2.6s)
- 15-multi-context-handover.test.mjs — ctx.a создаёт Контрагента, ctx.b в
  независимой сессии видит запись через filterList, ctx.a cleanup (14.5s, 4/4)

Live: 11/12 в полном прогоне. 04-selectvalue/direct-form флапает —
pre-existing, воспроизводится на baseline 95e4674 (03→04 sequence).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:24:24 +03:00
Nick Shirokov 95e4674825 test(01-navigation): M3 P1 — section/command/switchTab errors + navigateLink
section-error / command-error / switchTab error: проверка throw для
несуществующих имён.

navigateLink: link-type (Catalog.Контрагенты) + e1cib URL (с soft-skip
для платформ без поддержки e1cib через Shift+F11).

Live на webtest: 10/10 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:29:54 +03:00
Nick Shirokov 9751840cc8 test(09-filter): M3 P1 — exact, hidden-field, date, reference, unfilter-all
exact: filterList exact:true строго 1 совпадение.
hidden-field: filterList по неотображённому реквизиту через FieldSelector
DLB (КодКПП в синтетике нет — soft-skip).
date: filterList по колонке Дата поступления (синтетика выводит её в форму
списка Номенклатуры).
reference: filterList по ссылочной колонке Контрагент (форма списка ПН).
unfilter-all: unfilterList() полностью восстанавливает список.

unfilter-specific отложен — требует списка с видимой filter-панелью,
synthetic списки фильтруют без создания badge.
cancel-search/clear-input семантически дубликаты unfilter-all через
публичный API.
show-all-form требует quickChoice=true каталога с количеством > порога
(в синтетике нет).

Live на webtest: все 7 шагов passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:28:09 +03:00
Nick Shirokov f257bb428c test(12-formstate): M3 P1 — modal + tabs
modal: F4 на ref-поле открывает модальную форму выбора Контрагентов,
state.modal=true, formCount=2.

tabs: форма элемента Номенклатуры с двумя табами (Основное/Дополнительно)
возвращает state.tabs[].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:43:53 +03:00
Nick Shirokov 71e3691cf1 test(web-test): M3 P1 batch 1 — confirm-save-no/pending, more-menu, clear/ref-form, table checkbox/clear
02-crud: confirm-save-no (rollback при save:false), confirm-pending
(closeForm() без решения возвращает confirmation), more-menu (clickElement
'Ещё' возвращает submenu).

03-fillfields: clear (Shift+F4 через пустое значение), reference-non-quickchoice
(fillFields на quickChoice=false поле — method=dropdown через DLB; чистый
form-path требует hasPick && !hasSelect, такого поля в синтетике нет).

04-selectvalue: clear (selectValue '' → Shift+F4). show-all-form отложен —
требует quickChoice=true каталога с количеством > порога dropdown
(в синтетике нет).

05-table: checkbox (fillTableRow с Boolean), clear (Shift+F4 на ref-ячейке +
восстановление для последующего delete).

Live на webtest: все шаги проходят.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:40:27 +03:00
Nick Shirokov 1af318325d test(05-table): добавить явный tab-loop step с двумя числовыми полями
fillTableRow({Количество, Цена}, {row:1}) — purpose-built проверка inEdit
multi-cell tab-loop. method='direct' для обоих полей, значения
подставляются корректно (live на webtest).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:23:03 +03:00
Nick Shirokov 986480748e Merge branch 'dev' into feature/web-test-runner 2026-05-10 15:10:38 +03:00
Nick Shirokov 71309d2bc2 fix(tests): make cf-edit/set-home-page case platform-loadable
Кейс ссылался на CommonForm.* и DataProcessor.Поиск.Form.ФормаПоиска,
которых не было в workDir — verify-snapshots падал на db-load-xml,
а cf-validate Check 9 (валидация HP form refs) ловил это только под
--with-validation, поэтому в обычной регрессии проблема была не видна.

Поскольку CommonForm пока не умеет создавать ни meta-compile, ни form-add
(он привязывается к существующему объекту-владельцу), заменил ссылки
на формы Catalog/DataProcessor — DSL-покрытие сохранено целиком:
template, string-ref, height, visibility, русский синтаксис типа,
короткая и полная форма roles. Добавлен preRun из 10 шагов
(meta-compile + form-add + role-compile) для синтеза всех ссылок.

Поддержка CommonForm в meta-compile/form-add — отдельная задача.

verify-snapshots: 1/1 ✓ (db-load-xml, db-update проходят).
Полная регрессия: 349/349 ✓.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:57:30 +03:00
Nick Shirokov e1f81a0cde chore(tests): refresh snapshots after meta-compile QuickChoice + form-compile AutoTitle defaults
- QuickChoice<true→false> для Catalog (07b2ec3 «дефолты QuickChoice по реальным
  конфигам» — часть снапшотов уже обновили в dc0382c, эти были пропущены)
- автоген <Title>/<AutoTitle>/<TitleLocation>/<SavedData> в формах
  (76800fc «автоген Title из имени» и серия фич form-compile)
- column-group/ — новый снапшот для кейса из 4f9d9ae (был не закоммичен)

Все 349 unit-тестов и 6 integration-тестов зелёные. verify-snapshots
(платформенная загрузка) — 201/202; отдельный pre-existing fail
cf-edit/set-home-page (Check 9 не валидирует HP form refs) разбираем
отдельно, в дифф этой регенерации не входит.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:45:47 +03:00
Nick Shirokov e8cb5440d8 feat(switch): emit -ExecutionPolicy Bypass for codex target
Codex on Windows launches powershell.exe as a login-shell that loads
the user profile despite -NoProfile in our SKILL.md. With Restricted
ExecutionPolicy this spams "выполнение сценариев отключено". Add
-ExecutionPolicy Bypass for codex; keep canonical -NoProfile -File for
all other platforms.

Round-trip safe: cmd_install always copies fresh from .claude/skills/,
so switching codex→cursor strips the EP flag. cmd_switch_runtime
re-emits PS commands via normalize_ps_invocation each pass, so
in-place py↔ps in .codex/skills/ keeps the flag.

Also fix a pre-existing bug in cmd_switch_runtime: file-existence
check used repo_root() instead of project_dir, so in-place runtime
switch in a foreign project always tripped skip_runtime=True and
became a no-op. The bug was masked when project_dir == repo_root
(source-repo workflow).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:40:25 +03:00
Nick Shirokov c496047c6c fix(skills): force UTF-8 console encoding in 7 ps1 scripts
Codex runner on Windows launches PowerShell as login-shell and decodes
stdout/stderr without UTF-8, garbling Cyrillic output. The other 51 ps1
scripts already set `[Console]::OutputEncoding = UTF8`; bring these 7
in line and add `InputEncoding = UTF8` for symmetry.

Touched: epf-init, erf-init, form-add, form-remove, help-add,
template-add, template-remove. Versions bumped in both ps1 and py
headers to keep the pair in sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:16:44 +03:00
Nick Shirokov df9541470c chore(plugins): tighten descriptions and align marketplace entries with Codex schema
- Replace overreaching "полный цикл разработки... до записи видеоинструкций" pitch with grounded one-liner matching the GitHub repo description (XML/CLI abstractions + eyes & hands for web-client testing).
- Drop non-standard per-plugin `interface.shortDescription` from .agents/plugins/marketplace.json — MarketplaceInterface only describes `displayName`, openai/plugins keeps per-plugin entries minimal (name/source/policy/category).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:42:12 +03:00
Nick Shirokov 27ecfb707d revert(plugins): drop UTF-8 BOM on SKILL.md — Codex YAML parser breaks on it
BOM before `---` makes Codex's strict frontmatter parser reject every
SKILL.md ("Skipped loading 66 skill(s)"). Without BOM the skills load
and execute correctly; the only remaining issue is mojibake display of
Cyrillic in SKILL.md previews — that's a Codex rendering bug, not ours.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:27:08 +03:00
Nick Shirokov 35bec4a2a1 fix(plugins): prepend UTF-8 BOM to SKILL.md on codex branches
Codex on Windows opens SKILL.md without a declared encoding and
defaults to CP1252, mangling Cyrillic. Adding the BOM lets the loader
auto-detect UTF-8.

Applied only on codex/codex-py builds to leave other ports untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:19:10 +03:00
Nick Shirokov e635a5be52 fix(plugins): drop invalid authentication: OFF from Codex marketplace
Codex schema only accepts `ON_INSTALL` or `ON_USE`; the field is
optional, so omit it for an unauthenticated plugin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:26:09 +03:00
Nick Shirokov dfb258dd3f fix(plugins): prefix descriptions with [PowerShell]/[Python] for distinct cards
Codex plugin browser truncates the description, so the runtime tag
needs to appear at the start. Apply the same prefix to Claude
marketplace.json and plugin.json for consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:13:33 +03:00
Nick Shirokov 1866c1335a feat(plugins): Codex plugin support + Python plugin variant for Claude
- Add 1c-skills-py to .claude-plugin/marketplace.json (→ port-claude-code-py)
- New .agents/plugins/marketplace.json — Codex marketplace with PS + Py plugins
- Templates .github/templates/{codex,claude}-plugin.json.tmpl rendered by CI
- build-ports.yml: generate plugin manifests on port-codex/port-codex-py/port-claude-code-py; Codex version YYYY.M.D+sha7 auto-bumped per push
- README: install instructions for Codex + Py variant for Claude
- .gitignore: narrow .agents/ → .agents/skills/ so the marketplace is tracked

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:08:07 +03:00
Nick Shirokov 4813ec5cf2 docs(readme): inline "Другие платформы →" link, drop verbose intro line
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:41:20 +03:00
Nick Shirokov 06ad2f8f99 feat(ci): expand port matrix to all 13 platforms + bump deprecated actions
- Add 20 matrix entries: copilot, augment, cline, kilo, kiro, gemini, opencode, roo, windsurf, agents (PS+Py each)
- Total: 25 port-* branches (Claude Code PS = main, plus 13 × Py + 12 × PS)
- Bump actions/checkout@v4 → @v5, actions/setup-python@v5 → @v6 (removes Node.js 20 deprecation warnings)
- README: replace "соберите локально" placeholders with real branch links for all platforms

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:37:35 +03:00
Nick Shirokov c8ac191a01 feat(ci): port-branches workflow + README install matrix
- .github/workflows/build-ports.yml — auto-build orphan port-* branches on push to main (matrix: claude-code-py, cursor PS+Py, codex PS+Py)
- .github/templates/README.port.md.tmpl — minimal per-port README rendered in CI
- README — new "Версии навыков для разных платформ" section under intro (3 flagships × PS+Py), extended platform table with PowerShell/Python branch links, switch.py moved to "альтернативный способ" subsection
- README — "Work in progress" reworded to "Проект живой, активно развивается"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:29:52 +03:00
Nick Shirokov c9d83b1c92 docs(readme): add plugin install instructions
Document the recommended installation path via Claude Code plugin
marketplace:

  /plugin marketplace add https://github.com/Nikolay-Shirokov/cc-1c-skills
  /plugin install 1c-skills@cc-1c-skills

Plugin install becomes the recommended option; drop-in copy and
switch.py-based installs remain documented as alternatives for users
who prefer them or who target other AI platforms.

Closes #18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:40:38 +03:00
Nick Shirokov b1a7e414d0 fix(switch): runtime conversion for ${CLAUDE_SKILL_DIR} paths
After the SKILL.md refactor, paths are wrapped in double quotes and
contain ${CLAUDE_SKILL_DIR}. The legacy RX_PS/RX_PY regexes captured
the leading quote into the path group and didn't accept '$', '{', '}'
characters, breaking three places:

- classify_skill_runtime: misdetected runtime since RX_PY didn't match
  python invocations of variable paths
- check_missing_files: built file paths like '"${CLAUDE_SKILL_DIR}/...py'
  that never existed → false-positive missing → runtime switch skipped
- switch_runtime_content: failed to convert PS->Py / Py->PS for skills
  using the new path format

Fix:
- Regexes now capture optional surrounding quote separately and accept
  any non-whitespace non-quote chars in the path
- New helper expand_skill_path() resolves ${CLAUDE_SKILL_DIR} to the
  actual on-disk path for file existence checks (handles cross-skill
  references via ../<other>/ too)
- check_missing_files derives skill_name from skill_dir to drive the
  expansion

Verified via:
  python scripts/switch.py claude-code --project-dir <tmp> --runtime python
  python scripts/switch.py claude-code --project-dir <tmp> --runtime powershell
  python scripts/switch.py codex --project-dir <tmp>

All produce correct output with quotes preserved and cross-skill
references resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:56:26 +03:00
Nick Shirokov 7736ad68a0 feat(switch): handle ${CLAUDE_SKILL_DIR} for non-Claude targets
After SKILL.md refactor, paths use ${CLAUDE_SKILL_DIR} which Claude Code
substitutes natively. Other agents (Cursor, Codex, Copilot, etc.) don't
know about this variable, so switch.py now expands it to a literal path
when the target is not claude-code:

  ${CLAUDE_SKILL_DIR}/<rest>            -> <target_prefix>/<skill_name>/<rest>
  ${CLAUDE_SKILL_DIR}/../<other>/<rest> -> <target_prefix>/<other>/<rest>

For claude-code target the variable is left intact — drop-in install via
copy still works because Claude Code resolves it identically in
project, personal, and plugin scopes.

Verified by running:
  python scripts/switch.py codex --project-dir <tmp>
  python scripts/switch.py claude-code --project-dir <tmp>

Both produce correct output with 0 leftover variable literals in
non-Claude targets and full preservation in Claude target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:31:08 +03:00
Nick Shirokov 501bb58ddb feat(plugin): plugin and marketplace manifests
Add `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json` so
the repo can be installed via:

  /plugin marketplace add https://github.com/Nikolay-Shirokov/cc-1c-skills
  /plugin install 1c-skills@cc-1c-skills

Plugin name `1c-skills`, marketplace name `cc-1c-skills` (matches repo
name). Version is omitted in `plugin.json` so Claude Code uses the git
commit SHA — convenient for active development without manual bumps.

Closes part of #18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:30:32 +03:00
Nick Shirokov cd3a242b12 refactor(skills): portable script paths via ${CLAUDE_SKILL_DIR}
Replace literal `.claude/skills/<owner>/...` paths in SKILL.md with the
`${CLAUDE_SKILL_DIR}` variable that Claude Code substitutes at invocation
time. Same-skill references become `${CLAUDE_SKILL_DIR}/<rest>`,
cross-skill references (erf-* → epf-*) become
`${CLAUDE_SKILL_DIR}/../<other>/<rest>`. All paths now wrapped in double
quotes to handle install locations with spaces.

This unblocks plugin-mode installation: literal `.claude/skills/...`
paths fail when the skill is loaded from `~/.claude/plugins/cache/...`
or from a personal `~/.claude/skills/` install. Drop-in mode continues
to work because Claude Code resolves the variable in all install scopes.

Verified via pilot:
- Project drop-in (cc-1c-skills repo)
- Personal `~/.claude/skills/cf-info/`
- Plugin via `/plugin marketplace add <local-path>`

Scope: 61 SKILL.md, 125 path replacements (8 cross-skill).
Scripts unchanged — they already use \$PSScriptRoot and ../<sibling>
patterns that resolve correctly under the bundled cache layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:30:06 +03:00
Nick Shirokov 7561faf736 test(web-test): покрыть Tumbler через clickElement в radio-шаге
Tumbler-представление RadioButtonField не парсится fillFields, но варианты
видны в state.buttons[] и кликаются через clickElement. Уточнили шаг radio:
- RadioButtons (КатегорияЦены) → fillFields с method=radio
- Tumbler (СпособУчёта) → проверка наличия в buttons[] + clickElement('ФИФО')

Семантика Tumbler через fillFields остаётся как баг web-test/browser.mjs
(см. upload/web-test-bugs.md пункт 5), но рабочий путь интеракции есть.

10/10 smoke зелёные после рестарта Apache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 18:19:42 +03:00
Nick Shirokov 2849087fd9 test(web-test): покрытие quickChoice + radio (RadioButtons)
03-fillfields:
- reference-dropdown: переведён с Контрагент на Организация
  (после смены quickChoice Контрагенты идут через форму выбора)
- новый шаг radio: КатегорияЦены через method=radio (RadioButtons)

04-selectvalue:
- dropdown: переведён на Организация (quickChoice=true)
- новый шаг direct-form: Контрагент (quickChoice=false), method=form

Закрывает selectValue#3 dropdown (P0), selectValue#6 direct-form (P1),
fillFields#3 radio (P1) из coverage matrix.

Tumbler-представление радио (СпособУчёта) пока не покрыто — getFormState
не возвращает Tumbler в fields[]. Зафиксировано в upload/web-test-bugs.md
пункт 5.

10/10 smoke зелёные на webtest базе.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:17:19 +03:00
Nick Shirokov 105171cdc2 test(webtest-config): Организации/quickChoice + radio (RadioButtons+Tumbler)
Расширение синтетики под новые возможности meta-compile/form-compile,
закрывает три ветки coverage matrix:
- Catalog.Организации (quickChoice: true) → selectValue#3 dropdown (P0)
- Catalog.Контрагенты (дефолт quickChoice: false) → selectValue#6 direct-form (P1)
- form-compile radio с видами RadioButtons (КатегорияЦены) и Tumbler
  (СпособУчёта) → fillFields#3 radio (P1)

В шапку ПриходнаяНакладная добавлен реквизит Организация (dropdown ветка),
Контрагент остаётся на форме выбора. Фикстура ЗаполнитьОрганизации создаёт
2 организации (Альфа, Бета); первая подставляется в документы.

Платформенная верификация: build-webtest-db (45 шагов, 30.3s) зелёная,
db-create + db-load-xml + db-update проходят. Функциональный прогон
runner.mjs integration/build-webtest — 42 шага зелёные.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:58:27 +03:00
Nick Shirokov c9cd0d62ab Merge branch 'dev' into feature/web-test-runner 2026-05-04 13:15:49 +03:00
Nick Shirokov 4f9d9aee97 feat(form-compile): группировка колонок таблицы (ColumnGroup)
Новый DSL-ключ columnGroup со значением-ориентацией horizontal/vertical/inCell
для элемента <ColumnGroup> внутри columns таблицы. Поддерживает вложение,
showTitle/showInHeader/width, тихие синонимы ColumnGroup и ГруппаКолонок.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:48:35 +03:00
Nick Shirokov f3466e19fd docs(form-patterns): актуализация под новые возможности form-compile
- Архетипы «Форма обработки» и «Мастер»: кнопки действий перенесены
  с нижней горизонтальной группы на главную АКП формы (autoCmdBar)
- Конвенция «ГруппаКнопок» заменена на «ФормаКоманднаяПанель»
- Принцип компоновки №3: уточнено, что кнопки идут на АКП
- Сворачиваемые группы: исправлен пример — корректный DSL
  (group: collapsible, collapsed: true) вместо несуществующих
  ключей behavior/collapsed на vertical-группе
- Полный пример формы обработки переписан под autoCmdBar
- Из свойств мастера убран commandBarLocation: None (не нужен,
  когда мы сами наполняем АКП)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:10:10 +03:00
Nick Shirokov ebf92a8780 feat(form-compile): свойство collapsed для сворачиваемых групп
Добавлен ключ collapsed (для group=collapsible) → <Collapsed>true</Collapsed>:
группа создаётся уже свёрнутой. Раньше DSL умел только включать
сворачиваемое поведение, но начальное состояние задать было нельзя.

Также уточнено описание united: оно про выравнивание левого края полей
ввода (сквозное/локальное), а не про объединение рамок группы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:07:18 +03:00
Nick Shirokov 3d80191ca7 feat(form-compile): поддержка maxWidth/maxHeight для input и label
Добавлены численные maxWidth/maxHeight (XML <MaxWidth>/<MaxHeight>) —
типичный приём для ограничения растяжения поля при autoMaxWidth: false.
До этого DSL знал только булев autoMaxWidth, и ограничить ширину
числом было невозможно.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:58:36 +03:00
Nick Shirokov 6c60398406 feat(form-compile): авто-вид кнопки по контексту АКП
Кнопки внутри cmdBar/autoCmdBar/popup автоматически получают
CommandBarButton (или CommandBarHyperlink при type="hyperlink") —
указывать вид вручную не нужно. Резолвер прощающий: принимает и
короткие DSL-формы, и XML-имена в любом контексте.

Пример «Диалог загрузки файла» в SKILL.md и тест-кейс file-dialog
переведены на нативный паттерн с autoCmdBar вместо отдельной
горизонтальной группы кнопок внизу формы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:41:43 +03:00
Nick Shirokov 60de083a05 docs(1c-form-spec): RadioButtonField перенесён в статистику (~8.5% форм БП)
Замер по acc_8.3.24: 658 из 7723 форм содержат RadioButtonField
(всего 1389 элементов). Раньше ошибочно числился среди не встреченных.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:48:29 +03:00
Nick Shirokov 5690c82ab8 docs(form-dsl-spec): radio (RadioButtonField) в спецификации DSL
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:45:00 +03:00
Nick Shirokov dc0382cc06 chore(tests): обновлены снапшоты после meta-compile QuickChoice defaults
Снапшоты ссылочных реквизитов теперь содержат QuickChoice=false
в соответствии с дефолтами по реальным конфигам (см. 07b2ec3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:43:33 +03:00
Nick Shirokov b1e29253d5 feat(form-compile): RadioButtonField и словарь синонимов типов элементов
Поле переключателя с RadioButtonType (Auto/RadioButtons/Tumbler) и
ChoiceList (массив value+presentation). Толерантно к написанию модели:
русские имена тегов (ПолеПереключателя, RadioButtonField),
ВидПереключателя по-русски (Авто/Переключатель/Тумблер),
Перечисление.X.Y без EnumValue, синоним title для presentation,
автогенерация презентации из имени значения.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:43:26 +03:00
Nick Shirokov 07b2ec36af feat(meta-compile): дефолты QuickChoice по реальным конфигам
Catalog/CCT/CoA/CoCT/ExchangePlan: дефолт false (раньше true). Enum: дефолт true (теперь параметризовано). Цифры из acc_8.3.27 + erp_8.3.24 — у Catalog ~95% объектов QuickChoice=false, у Enum ~99% true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 17:34:16 +03:00
Nick Shirokov c1a0a54971 feat(web-test): --record и export const params
Раннер v1.7.

T5 --record: startRecording перед каждым тестом, stopRecording
после (и в passed, и в failed ветке). Файл
{reportDir}/{testIdx}-{slug}.mp4. testResult.video содержит путь.
В Allure — attachment типа video/mp4. config.record читается
тоже. Использует существующую инфраструктуру browser.mjs.

T6 export const params: материализация в N тестов на этапе
discovery. Имя через {key}-шаблон в mod.name (например
'demo {type}'); если шаблона нет — суффикс [index]. Тест-функция
получает param как второй аргумент: default(ctx, param).
В отчёте каждый набор — отдельная test entry с собственным uuid
в Allure / testcase в JUnit.

Live-проверка:
- params: 2 теста с именами demo A / demo B из шаблона.
- record: mp4 91KB на 6-секундном тесте, путь в JSON и
  Allure attachment video/mp4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:19:52 +03:00
Nick Shirokov 927c0827f3 feat(web-test): --format=allure и --format=junit
Раннер v1.6. Реализованы оба формата отчётов из spec §9.

allure: {reportDir}/{uuid}-result.json на каждый тест. uuid через
randomUUID, labels из tags, steps рекурсивно с attachments из
step.screenshot, statusDetails для упавших шагов и тестов.
Пропускает skipped (нет start/stop).

junit: один XML в --report=path.xml. Валидация: --format=junit
требует --report=. xmlEscape для name/message/trace. <failure>
для упавших, <skipped/> для пропущенных, <system-out> со ссылкой
на screenshot.

Валидация формата (json|allure|junit) на старте cmdTest.
testResult теперь хранит start/stop в мс — нужно для Allure
и полезно в JSON-отчёте.

Live-проверка: 01-navigation в Allure (5 шагов с attachments,
все ссылки на существующие PNG); JUnit с passed и forced-fail
(спецсимволы корректно экранированы).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:03:31 +03:00
Nick Shirokov 56cd18a6b4 feat(web-test): --screenshot=on-failure|every-step|off + --report-dir
Раннер v1.5. Парсит --screenshot и --report-dir, мерж с config.screenshot.
- every-step: после успешного step() пишет {reportDir}/{testIdx}-{stepIdx}-{slug}.png,
  путь в step.screenshot.
- off: ни пошаговых, ни error-shot.
- on-failure (default): error-shot уехал из .claude/skills/web-test/
  в {reportDir}/error-{testIdx}-{slug}.png.

reportDir фоллбэчит: --report-dir → dirname(--report) → testDir.

Известная нестыковка: error-shot из buildContext/executeScript остаётся в
.claude/skills/web-test/error-shot.png — затронем при T2 (Allure).

Live-проверка: 01-navigation с every-step (5 PNG), off (пусто),
default on-failure на стуб-failing тесте (error-shot в reportDir).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:54:38 +03:00
Nick Shirokov 3ac1d425cd test(11-report): DCS-отчёт ОстаткиТоваров + smoke с быстрым фильтром
Синтетика: добавлен template-add ОсновнаяСхемаКомпоновкиДанных к отчёту
(без него skd-compile писал Template.xml в незарегистрированный путь),
переписан DSL skd-compile — fields внутри dataSets, типы полей, totalFields,
явный settingsVariants со structure и быстрым отбором по Номенклатуре
(@off @user @quickAccess).

Тест 11-report покрывает: регистрацию команды в подсистеме, открытие формы
отчёта с дефолтной кнопкой Сформировать, видимость и структуру быстрого
DCS-фильтра, формирование отчёта, применение фильтра через selectValue
(auto-enable чекбокса + значение), пересчёт с фильтром, снятие фильтра
через fillFields toggle off с восстановлением исходных данных.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:22:22 +03:00
Nick Shirokov 3c596f4550 test(12-formstate): smoke базовых полей getFormState
Покрывает форму списка (form, formCount, openForms, tables, buttons)
и форму элемента (fields с label и value, проверка по конкретному
полю Наименование).
2026-05-02 20:15:20 +03:00
Nick Shirokov 36d29a51a9 test(09-filter): smoke filterList simple-search и advanced-column
Покрывает:
- filterList('Север') — поиск по всем колонкам списка Контрагенты
- filterList('Север', { field: 'Наименование' }) — фильтр по
  конкретной колонке через расширенный поиск
- unfilterList — восстановление исходного набора

Третий запланированный кейс (text-field filter) семантически совпадает
с advanced-column когда колонка строкового типа — оставлен на регресс P1.
2026-05-02 20:11:58 +03:00
Nick Shirokov 11e961c816 test(07-tabs): smoke переключение страниц формы Основное/Дополнительно
Покрывает clickElement по имени страницы как механизм переключения
вкладок формы. Используем форму элемента Номенклатура: page1
показывает шапку (Артикул, ВидНоменклатуры, ...), page2 — Дополнительно
(ЕдиницаИзмерения, Комментарий). Verify: набор state.fields различен
после переключения и совпадает после возврата.
2026-05-02 20:07:46 +03:00
Nick Shirokov 05ca810461 test(06-document): сверка с Комментарий=docId, защита от грязной базы
Раньше verify-list брал первый попавшийся проведённый документ Север —
если в базе уже лежал проведённый Север из прошлого прогона, тест
проходил даже если текущий не сохранился. Теперь среди кандидатов
открываем каждый и сверяем Комментарий с уникальным docId текущего
прогона; ассерт срабатывает только при совпадении.
2026-05-02 20:04:38 +03:00
Nick Shirokov a0407b74dc test(06-document): проверка закрытия по смене номера формы вместо костыля
Раньше использовалось отсутствие поля Контрагент после Провести и закрыть
как косвенный признак закрытия — это работало, но было привязано к
конкретному реквизиту накладной. Заменил на сравнение state.form до и
после: номер активной формы меняется (11 → 5), это прямой и общий
признак, что мы переключились с формы документа на другую.
2026-05-02 19:58:56 +03:00
Nick Shirokov 3aad254399 test(06-document): smoke workflow проведения накладной
Создание, заполнение шапки и табличной части, Провести и закрыть,
проверка появления документа в списке с Проведён=Да.

Проверка закрытия формы документа: в синтетике web-test форма списка и
форма документа делят один слот (formCount=1 в обоих состояниях),
поэтому используем признак отсутствия поля Контрагент в текущем
state.fields после Провести и закрыть — если поле есть, мы остались
на форме документа.
2026-05-02 19:54:34 +03:00
Nick Shirokov 07753921be test(05-table): smoke add/edit/delete для табличной части накладной
Покрывает работу с табличной частью Товары документа Приходная накладная:
- fillTableRow с add:true добавляет строки последовательно
- fillTableRow с row:N редактирует существующую строку (Tab-навигация)
- deleteTableRow удаляет строку по индексу

Закрытие формы без сохранения (save:false) — соответствует новой
семантике после фикса form-compile (SavedData).
2026-05-02 19:49:19 +03:00
Nick Shirokov ba0c71fa45 test(smoke): починить 01-navigation и 04-selectvalue после фикса form-compile
01-navigation: первое открытое окно 1С имеет form=0 (number), и
assert.ok(state.form, ...) валился на falsy при первом запуске сессии.
Сменил на state.form != null.

04-selectvalue: явный save:false при закрытии модифицированной формы
накладной — после фикса SavedData=true главного реквизита платформа
требует решения по confirmation dialog.
2026-05-02 19:45:15 +03:00
Nick Shirokov 33c9fdade0 test(03-fillfields): boolean → CheckBoxField, явный save:false при закрытии
После фикса form-compile (kind=check для Boolean + SavedData=true для
главного реквизита) Активен передаётся как настоящий boolean (toggle),
getFormState возвращает value:true/false. Закрытие модифицированных форм
теперь требует явного save:false — иначе платформа показывает
confirmation dialog «Записать?».
2026-05-02 19:40:26 +03:00
Nick Shirokov 1c1fe7b2d9 test(02-crud): убрать устаревший комментарий про T11/SavedData
После фикса form-compile (a59be4b SavedData=true для главного реквизита)
canonical confirm-save-yes flow работает без ручного патча Form.xml —
предупреждение в шаге неактуально.
2026-05-02 19:26:56 +03:00
Nick Shirokov 0bd2587e74 test(build-webtest-config): Активен как check вместо input для Boolean
После фикса form-compile (Дефект 2: kind=check → CheckBoxField) булевый
реквизит Активен в форме элемента и форме списка Номенклатуры теперь
описывается как check — рендерится настоящим чекбоксом.
2026-05-02 19:24:50 +03:00
Nick Shirokov 6f17b1c2f6 Merge branch 'dev' into feature/web-test-runner 2026-05-02 19:08:28 +03:00
Nick Shirokov b4514dc350 feat(form-compile): AutoMaxWidth=false по умолчанию для multiLine InputField
В реальных формах ERP/БП у ~60% многострочных полей ввода явно стоит
АвтоМаксимальнаяШирина=Ложь. Теперь form-compile проставляет это
автоматически при multiLine: true, если пользователь не задал
autoMaxWidth явно.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:05:50 +03:00
Nick Shirokov 8ad5d80f7f feat(form-compile): автоподстановка RowPictureDataPath для DynamicList с MainTable
Document/InformationRegister/AccumulationRegister List-генераторы теперь
прописывают `Список.DefaultPicture` (как делает ERP/БП в 594/600 форм
списка документов). Плюс fallback в эвристике DynamicList-таблицы:
если у главного реквизита есть `settings.mainTable`, поле
подставляется автоматически и для ручного DSL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:39:29 +03:00
Nick Shirokov fc19df604b chore(tests): --help в runner и verify-snapshots, синхронизация README
Чтобы --help/-h/? не запускали полный прогон тестов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:17:24 +03:00
Nick Shirokov 76800fc92b feat(form-compile): автоген Title из имени для узлов без источника синонима
Реквизиты формы, команды, страницы, попапы и декорации теперь получают Title,
сгенерированный из CamelCase-имени, если в DSL он не задан явно. Поля с path
и кнопки с command по-прежнему опускают Title — синоним подхватится платформой.
Главные реквизиты (Объект/Список/Запись с main=true) исключены.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:17:19 +03:00
Nick Shirokov 36cd63d8bb fix(form-compile): AutoTitle=false по умолчанию при заданном Title формы
Когда у формы задан Title (через defn.title или properties.title), эмитим
AutoTitle=false если пользователь явно его не указал. Иначе платформа
добавляет суффикс синонима и получается двойной заголовок (Номенклатура:
Номенклатура). В выгрузках ERP так делает ~95% форм с form-level Title.

Снапшоты form-compile/form-compile-from-object обновлены под новое поведение.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:22:48 +03:00
Nick Shirokov e216db5734 fix(form-compile): default TitleLocation=Right для CheckBoxField
Платформенный default TitleLocation для CheckBoxField — Left, что почти
никогда не соответствует UX-ожиданиям. В acc 8.3.27 для CheckBoxField:
Right (явно): 811, без тега (=Left): 406, None: 140, Left: 14, Top: 3 —
доминирующий паттерн «заголовок справа от флажка».

Эмитим <TitleLocation>Right</TitleLocation> по умолчанию для check.
Переопределяется через titleLocation: 'Left' / 'None' / 'Top' / 'Bottom'.

v1.11. Обновил 5 snapshot'ов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:59:31 +03:00
Nick Shirokov a59be4b914 fix(form-compile): SavedData=true для object/RecordManager главного реквизита
Без <SavedData>true</SavedData> платформа не маркирует форму как modified
при изменении главного реквизита — confirmation dialog при Esc не появляется,
canonical flow «изменил → Esc → Да» сломан.

Правило выведено из реальной выгрузки acc 8.3.27: SavedData ставится только
для редактируемых форм с типом *Object.X (Catalog/Document/ChartOf…/
ExchangePlan/BusinessProcess/Task) или *RecordManager.X. DynamicList/Report/
DataProcessor/ConstantsSet/RecordSet — не трогаем, отдаём решение DSL через
явный savedData: true.

Поднял версию до v1.10. Обновил 5 snapshot'ов (формы элементов/документа).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:51:43 +03:00
Nick Shirokov 36ad686316 feat(web-test): smoke-тест 04-selectvalue (dropdown быстрый выбор)
Один P0 кейс из coverage matrix:
- dropdown: selectValue('Контрагент', 'ООО Север') → method='dropdown'
  на форме новой ПриходнойНакладной (CatalogRef + малый список)

API возвращает form state с .selected = {field, search, method}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:57:37 +03:00
Nick Shirokov 66e37fb8cc feat(web-test): smoke-тест 03-fillfields (text, dropdown, date, reference)
2 шага, 5 типов полей зелёные на синтетике webtest:
- text (paste): Артикул на форме Номенклатура
- dropdown (Да/Нет): Активен — Boolean рендерится как Да/Нет селектор
- dropdown (EnumRef): ВидНоменклатуры
- date (paste): ДатаПоступления
- reference (dropdown CatalogRef): Контрагент в новой ПриходнаяНакладная

NB: 1C рендерит Boolean-атрибут не как чекбокс, а как dropdown «Да/Нет»
(actions: ["select"]) — fillFields правильно определяет это.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:55:14 +03:00
Nick Shirokov 99c77e1dde fix(web-test): 02-crud использует canonical closeForm({save:true})
Гипотеза о баге fillField paste была ошибочной — реальная причина в form-compile
который не эмитит <SavedData>true</SavedData> для MainAttribute главной формы.
Платформа без SavedData не трекает modified-state, confirmation dialog не
появляется.

Платформенная верификация на патченной Form.xml: closeForm({save:true})
после fillField корректно ловит confirmation, жмёт «Да», изменения
сохраняются. См. T11 в upload/web-test-runner-tasks.md.

ВНИМАНИЕ: тест зависит от ручного патча Form.xml. После прогона
build-webtest-db.mjs тест упадёт до фикса form-compile (T11).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:44:39 +03:00
Nick Shirokov 8d6612027f feat(web-test): smoke-тест 02-crud (open-item, close-clean, read, save)
4 шага зелёные на синтетике webtest:
- read: список Контрагентов отдаёт колонки/строки/total
- open-item: dblclick открывает форму элемента
- close-clean: Escape без изменений закрывает форму без диалога
- save-via-button: fillField + «Записать и закрыть» → значение сохраняется

confirm-save-yes (P0 из coverage matrix) отложен — fillField через paste не
выставляет 1C "modified" флаг, confirmation dialog не появляется. Зафиксировано
в upload/web-test-runner-tasks.md как T11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:34:37 +03:00
Nick Shirokov c3b67a18cb feat(tests): build-webtest-db скрипт для постоянной webtest базы
Заменяет одноразовый platform-webtest-config.test.mjs на скрипт сборки в
постоянные пути из .v8-project.json (tests/skills/.cache/webtest-config
+ C:\edt\IB\webtest). Переиспользует steps из build-webtest-config.test.mjs.

Generic platform-config.test.mjs уже покрывает regression «платформа принимает
сборку» — отдельный синтетический тест дублировал.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:23:42 +03:00
Nick Shirokov 4d1b66638c Merge branch 'dev' into feature/web-test-runner 2026-05-02 14:54:50 +03:00
Nick Shirokov 4b8d3b67dc fix(tests): platform-verifiable form-info DSL + external setup в verify-snapshots
- form-info/{rich-form,simple-form}: тип реквизита Объект исправлен
  с ExternalDataProcessorObject на DataProcessorObject — preRun создаёт
  обычную обработку, и платформа отвергала несоответствие XDTO-исключением.
- verify-snapshots.mjs: поддержка setup="external:<path>" — копирует
  внешний дамп (выгрузку ERP/БП) в workDir, пропускает cf-init и
  авто-регистрацию объектов через cf-edit. Платформенная загрузка
  целой вендорской конфы скипается — кейсы real-acc-form-* проверяют
  скрипт против реального XML, а не пригодность всей БП к чистой базе.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:31:18 +03:00
Nick Shirokov 6650d2b516 feat(form-info): показывать главную AutoCommandBar формы с учётом CommandBarLocation
Раньше AutoCommandBar (id=-1) полностью скрывалась как companion-элемент.
Из-за этого модель не видела главную панель и часто добавляла избыточную
дополнительную cmdBar снизу формы по образцу старых сгенерённых форм.

Теперь:
- AutoCommandBar формы выводится отдельным разделом с флагами
  (autofill/no-autofill, align=...) и списком кастомных кнопок.
- Позиция секции зависит от свойства формы CommandBarLocation:
  Auto/Top — над деревом элементов (нативное поведение платформы),
  Bottom — под деревом, None — секция скрыта.
- Если панель пустая с дефолтным autofill — выводится одной строкой.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:04:26 +03:00
Nick Shirokov 6b63177687 feat(form-compile): не эмитить HorizontalAlign по умолчанию + чистка SKILL.md
- HorizontalAlign не эмитится, если не задан явно через autoCmdBar.horizontalAlign.
  Раньше был хардкод Right; платформенный дефолт — Left, эталонные формы
  типовых (Бригады/ФормаСписка) тег вообще не содержат.
- horizontalAlign принимает Left/Center/Right.
- SKILL.md: убран раздел «Эвристики компилятора» (внутренняя кухня скрипта),
  сжато описание autoCmdBar и main — модель пишет DSL явно, помощь
  компилятора видна в [INFO]/[WARN]-логе.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:54:57 +03:00
Nick Shirokov 13174b63b1 feat(form-compile): нативная AutoCommandBar формы + autoCmdBar DSL
- Эвристика главной АКП: без cmdBar/autoCmdBar остаётся Autofill=true
  (как в Конфигураторе), с cmdBar — Autofill=false (обратная совместимость).
- Новый элемент autoCmdBar для наполнения главной АКП кастомными кнопками.
- Тихие синонимы commandBar↔cmdBar, autoCommandBar↔autoCmdBar.
- Инференс main-реквизита по типу (*Object.*, *RecordSet.*, DynamicList,
  ConstantsSet) — единственный кандидат проставляется молча с [INFO].
- Эвристика DynamicList → таблица: tableAutofill=false +
  commandBarLocation=None для привязанной таблицы (соответствие ERP).
- Косметика: <Autofill>true</Autofill> не эмитится явно.

Snapshot'ы form-* также обновлены до актуального состояния cf-init
(Ext/ClientApplicationInterface.xml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:46:23 +03:00
Nick Shirokov 433a2955b5 fix(form-compile): лояльно стрипать cfg: префикс главного реквизита
Раньше тип `cfg:CatalogObject.X` мимо regex попадал в fallback и получал
второй `cfg:` поверх → `cfg:cfg:...`, db-load-xml падал. Теперь
Resolve-TypeStr срезает ведущий `cfg:` сразу, обе формы записи валидны.
Заодно сообщение об ошибке FormDataStructure согласовано с примером —
без префикса.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 19:32:53 +03:00
Nick Shirokov 42b96bbd21 feat(cf-*): set-home-page + drill-down -Section home-page + form-ref валидация
- cf-edit: новая операция set-home-page перезаписывает Ext/HomePageWorkArea.xml.
  DSL принимает template (OneColumn/TwoColumnsEqualWidth/TwoColumnsVariableWidth),
  left/right с записями форм (строка или объект form/height/visibility/roles).
  Тихая нормализация ссылок: русские типы, 3-сегмент → авто-Form, файловые пути
- cf-info: краткая HP-сводка (template + счётчики) в overview/full, детальный
  вид через -Section home-page (alias -Name) с раскладкой и переопределениями ролей
- cf-validate: Check 9 — валидация ссылок на формы из HomePageWorkArea и
  Default*Form свойств; битая ссылка → error
- reference.md: убран реализационный шум (canonical sort, авто-нормализация форм,
  panelDef detail, секция авто-валидации); путь src/ в примерах вместо репо-специфичных

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 19:13:18 +03:00
Nick Shirokov 363a9f34f2 Merge dev: cf-info раскладка, cf-edit set-panels с русскими синонимами 2026-05-01 17:06:51 +03:00
Nick Shirokov bd3b40852a feat(cf-edit): set-panels принимает русские имена панелей silently
cf-info отображает «Открытых», «Разделов», «Избранного», «История», «Функций»
(совпадает с подписями Конфигуратора). Если модель копирует эти названия в
set-panels value — теперь они тихо мапятся в каноничные английские алиасы.
Документация и сообщения об ошибках упоминают только английские формы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:05:38 +03:00
Nick Shirokov 336dade274 feat(cf-edit): операция set-panels для раскладки панелей
JSON-DSL уровня имён: алиасы sections/open/favorites/history/functions
для платформенных uuid, объект {group:[...]} для стека (даёт <group>-
вложенность как у Конфигуратора), несколько записей в одной стороне =
соседние теги (рядом). Файл Ext/ClientApplicationInterface.xml
перезаписывается полностью; panelDef для всех 5 панелей пишется всегда.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:03:57 +03:00
Nick Shirokov 99e9c1e0b8 feat(cf-info): показывать раскладку панелей в overview/full
Читает Ext/ClientApplicationInterface.xml (если есть) и выводит секцию
«Раскладка панелей» с маппингом UUID → имя для 5 платформенных панелей.
Стек панелей внутри одной стороны отображается как «Стек(a, b)»,
несколько отдельных тегов стороны (рядом) — через « | ».

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:57:53 +03:00
Nick Shirokov 4f8ce7b747 chore(web-test): убрать инлайн ClientApplicationInterface.xml
Файл теперь генерируется самим cf-init с ERP-дефолтом (см. предыдущий
коммит на dev), отдельный writeFile в build-webtest-config больше не нужен.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:48:13 +03:00
Nick Shirokov fc48d68ed1 Merge dev: cf-init создаёт Ext/ClientApplicationInterface.xml 2026-05-01 16:46:30 +03:00
Nick Shirokov 3c3ed2ff46 feat(cf-init): генерить Ext/ClientApplicationInterface.xml с ERP-дефолтом
Без этого файла веб-клиент 1С рендерит секции icon-only (без подписей),
а web-test их не видит. Дефолтная раскладка как в типовых ERP/БП ≥ 8.3.24:
панель открытых сверху, панель разделов слева; функций/избранного/истории
объявлены через panelDef но не размещены.

Расширил docs/1c-configuration-spec.md § 4.2 моделью раскладки и таблицей
UUID 5 платформенных панелей. Обновил снапшоты cf-init под новый файл.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:43:39 +03:00
Nick Shirokov 3e34ec0bdd fix(web-test): заголовки и стиль вкладок на форме Номенклатуры
Page элементы в DSL получали name (через ключ 'page'), но не получали
title, поэтому вкладки рендерились пустыми квадратиками. Также Pages
без явного pagesRepresentation отображались в режиме None (без табов).

- Добавил title к каждой Page (Основное, Дополнительно)
- pagesRepresentation: 'TabsOnTop' на Pages

После: getFormState().tabs возвращает [{name:'Основное'},{name:'Дополнительно'}]
вместо пустого массива.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:33:19 +03:00
Nick Shirokov fff2e83960 fix(web-test): починить runtime синтетики для тонкого клиента
Два бага, найденные при попытке запустить синтетическую ИБ через
web-publish + web-test:

1. ОбщиеФункции без ServerCall=true — ManagedApplicationModule (клиент)
   не мог звать процедуры серверного модуля напрямую. ПриНачалеРаботыСистемы
   падал с ошибкой компиляции в runtime, страница не догружалась. Добавил
   serverCall: true в DSL meta-compile.

2. Без Ext/ClientApplicationInterface.xml панель разделов рендерилась
   icon-only (без подписей), web-test navigateSection не находил секции.
   Добавил writeFile-шаг с раскладкой панелей как в acc/erp:
   - top: панель разделов (8e10648b...) + панель информации (cbab57f2...)
   - left: панель функций текущего раздела (b553047f...)

Проверено end-to-end: после пересборки runner-ом + web-publish + start
работают navigateSection, openCommand, readTable. Фикстуры (4 контрагента,
25 номенклатуры в группах, 3 документа) автоматически заполняются при
первом старте через ManagedApplicationModule → ОбщиеФункции.ЗаполнитьФикстурыЕслиНужно.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:12:43 +03:00
Nick Shirokov 1ff209849f feat(web-test): первоначальное заполнение фикстур (M1 Step 5)
Покрытие matrix #9 — данные для smoke-тестов:
- Константа ДанныеЗаполнены (Boolean) — флаг идемпотентности
- ОбщиеФункции.ЗаполнитьФикстурыЕслиНужно() — транзакционно создаёт:
  * 4 контрагента (ООО Север/Юг/Восток, АО Запад)
  * 25 номенклатуры в группах Товары (15) и Услуги (10)
  * 3 приходных накладных по 3 строки
- Ext/ManagedApplicationModule.bsl с ПриНачалеРаботыСистемы — вызывает
  заполнение при первом старте тонкого клиента

Платформенная верификация компилирует BSL (43 шага, 23.7s). Реальное
выполнение заполнения произойдёт при первом подключении web-test
runner-а к синтетической базе.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:36:13 +03:00
Nick Shirokov 1a8415283e chore(web-test): убрать избыточный cf-edit (объекты регистрируются автоматически)
meta-compile/subsystem-compile/role-compile сами добавляют записи в
Configuration.xml. cf-edit в каждом прогоне рапортовал Added: 0 — был
no-op + дублировал список объектов, который надо было синхронизировать
руками при каждом изменении.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:32:48 +03:00
Nick Shirokov db1e78a534 feat(web-test): подчинённый каталог КонтактныеЛица (M1 Step 4)
Покрытие matrix #8 — getFormState.navigation (12-formstate/subordinate-nav):
- Catalog.КонтактныеЛица с Owner=Catalog.Контрагенты
- Реквизиты: Должность, Телефон
- ФормаЭлемента (с владельцем) + ФормаСписка
- Регистрация в Configuration + Subsystem.Склад + Role

Платформенная верификация: 41 шаг, 23.8s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:30:15 +03:00
Nick Shirokov a828f1847f feat(web-test): обработка ТестовыеОшибки + bsl-модули (M1 Step 3)
Покрытие matrix #6 — errors balloon/messages/modal (10-validation,
fetchErrorStack Path 2):
- ОбщиеФункции.ПоказатьСообщение() → Сообщить("Тестовое сообщение")
- ОбщиеФункции.ВызватьТестовоеИсключение() → ВызватьИсключение
- DataProcessor.ТестовыеОшибки + ФормаОбработки с двумя кнопками,
  каждая делает клиент→сервер вызов соответствующей процедуры
- Регистрация в Configuration + Subsystem.Администрирование

Runner расширен step-типом writeFile — записывает произвольный текст
(обычно Module.bsl) в workDir. Нет навыка для модификации bsl-кода
модулей, и плодить отдельный навык под одну задачу избыточно.

Платформенная верификация: 36 шагов, 21.2s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:27:38 +03:00
Nick Shirokov 3e8159b591 feat(web-test): формы списков с правильными колонками (M1 Step 2)
Расширение синтетики (пункты 3, 4, 5 из M1):
- Контрагенты.КодКПП: новый String реквизит, НЕ выводимый в форму списка
  (для теста filterList #5 — FieldSelector DLB по скрытой колонке)
- Catalog.Контрагенты.ФормаСписка: Code, Description, ИНН, Телефон, Адрес
- Catalog.Номенклатура.ФормаСписка: Code, Description, Артикул,
  ВидНоменклатуры, ДатаПоступления, Цена, Активен (date-колонка для
  filterList #6)
- Document.ПриходнаяНакладная.ФормаСписка: Date, Number, Контрагент, Posted
  (reference-колонка для filterList #7)

Платформенная верификация: 31 шаг, 21.4s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:23:38 +03:00
Nick Shirokov 57bb964c1e feat(web-test): ссылочные типы и Boolean в синтетике (M1 Step 1)
Расширение build-webtest-config под coverage matrix (пункты 1, 2, 7
из upload/web-test-runner-tasks.md M1):
- Перечисление КатегорииЦен (для будущего radio-button теста)
- Номенклатура.ВидНоменклатуры → EnumRef.ВидыНоменклатуры
- Номенклатура.КатегорияЦены → EnumRef.КатегорииЦен
- ПриходнаяНакладная.Контрагент: String → CatalogRef.Контрагенты
- ПриходнаяНакладная.Товары.Номенклатура: String → CatalogRef.Номенклатура
- ПриходнаяНакладная.Товары.Согласовано: новый Boolean (для checkbox
  в grid, fillTableRow ветка #6)
- Формы Номенклатура и Документ обновлены под новые поля
- Subsystem.Склад: добавлены Enum.* в content
- Configuration.xml регистрирует Enum.КатегорииЦен

Платформенная верификация (platform-webtest-config.test.mjs) зелёная,
25 шагов 16.7s.

Гэп: form-compile не умеет рендерить RadioButtonField — представление
КатегорияЦены остаётся обычным input. Будет отдельной задачей перед
тестами P1 fillFields/radio.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:21:22 +03:00
Nick Shirokov 41c4b6b1f7 fix(skills/tests): cleanupWorkspace терпимо переживает EBUSY от 1cv8
После платформенных тестов (db-create/db-load-xml/db-update) Windows
держит файловые хэндлы 1cv8 ещё несколько сотен миллисекунд. rmSync без
ретраев падал EBUSY на Roles/.../Rights.xml, и uncaught-ошибка в finally
рушила весь node-процесс — теряли результат теста.

Теперь rmSync с maxRetries: 10, retryDelay: 200 (≈2с буфер) и try/catch
вокруг — в худшем случае warning + лишняя tmp-папка вместо краша.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:21:08 +03:00
Nick Shirokov ffb0ee740d fix(web-test): восстановить синтетическую конфу + платформенная верификация
build-webtest-config упал после ужесточения form-compile (запрет runtime-типа
FormDataStructure для главного реквизита). Перевёл типы на конкретные
CatalogObject.X / DocumentObject.X без cfg:-префикса. Добавил
platform-webtest-config.test.mjs — переиспользует шаги сборки и в хвосте
делает db-create + db-load-xml + db-update. Зелёный, 24 шага.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:12:45 +03:00
Nick Shirokov f5e487096f Merge branch 'dev' into feature/web-test-runner 2026-05-01 11:58:28 +03:00
Nick Shirokov d75b4d96ca fix(verify-snapshots): структурные зависимости с preRun и ChartOfAccounts
getStructuralDeps теперь сканирует все inputs (caseData.input + preRun.input),
а не только верхний — без этого регистры, созданные через preRun, оставались
без документа-регистратора и платформа отвергала конфиг с
«Ни один из документов не является регистратором».

Добавлен случай ChartOfAccounts: при maxExtDimensionCount>0 (или непустых
extDimensionAccountingFlags) и без явной ссылки extDimensionTypes
автоматически создаётся стаб ChartOfCharacteristicTypes и линкуется через
meta-edit modify-property уже после preRun. Для этого getStructuralDeps
возвращает теперь { deps, hostEdits }, а в основной поток добавлен Step 3.5
(host-level structural edits).

InformationRegister с writeMode=Subordinate/RecorderSubordinate тоже
получает документ-регистратор.

Полный прогон verify-snapshots: 193/193.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:12:12 +03:00
Nick Shirokov 704fbc9f28 feat(verify-snapshots): платформенная верификация mxl-compile
Готовый Template.xml оборачивается в исходники EPF: epf-init создаёт
скелет, template-add регистрирует SpreadsheetDocument-макет, MXL копируется
поверх дефолтного, далее epf-build реально проверяет, что платформа
принимает разметку. mxl-compile убран из STANDALONE_SKILLS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:59:06 +03:00
Nick Shirokov cd74afa5e0 feat(verify-snapshots): платформенная верификация template-add и help-add
После main-скрипта определяется тип артефакта в workDir: Configuration.xml
→ обычная загрузка конфигурации, иначе по корню *.xml выбирается ветка
epf-build (.epf для ExternalDataProcessor, .erf для ExternalReport). Для
кейсов с cf-init теперь configDir переустанавливается на workDir, так что
конфиг действительно грузится в БД, а не пропускается.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:42:00 +03:00
Nick Shirokov 370873511d feat(verify-snapshots): платформенная верификация epf-init и erf-init
EPF_SKILLS превращён из «skip» в реальную сборку через epf-build —
строится .epf или .erf по расширению из map. Имя источника берётся
из caseData.params.name. erf-init добавлен в DEFAULT_SKILLS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:25:20 +03:00
Nick Shirokov 9ced410d8f feat(verify-snapshots): платформенная верификация skd-edit
skd-edit добавлен в SKD_PLATFORM_VERIFY — результат заворачивается в ERF и
собирается через epf-build, как уже сделано для skd-compile. Резолвер пути
шаблона теперь учитывает params.templatePath (фолбэк: outputPath, Template.xml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:09:38 +03:00
Nick Shirokov 50ba38a9f6 docs(skd-compile): корректные имена ресурсов оборотов в примере @autoDates
Виртуальная таблица .Обороты(...) возвращает ресурсы регистра с
суффиксом «Оборот» — Продажи.Количество в запросе к Обороты не
существует, нужно Продажи.КоличествоОборот. Чтобы поле в наборе
осталось «Количество» (и совпадало с totalFields/fields), добавлен
алиас КАК.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:08:32 +03:00
Nick Shirokov d4b105bbe4 docs(skd-compile): починить пример «С ресурсами, параметрами и @autoDates»
В прежнем примере @autoDates не приносил пользы (запрос к физической
таблице регистра, а не к виртуальной Обороты), а Организация
использовалась в filter и structure, но не выбиралась в запросе и
не была описана в fields — так схема не собралась бы или собралась
бы с пустыми колонками.

Чиню:
- запрос → РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода)
- добавлено поле Организация: СправочникСсылка.Организации @dimension
- selection — явный список без "Auto" (все поля и так перечислены)
- dataParameters: "auto" вместо ручного перечисления

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:00:33 +03:00
Nick Shirokov 204a262746 feat(verify-snapshots): реальная платформенная проверка skd-compile
До сих пор для skd-compile (как и других STANDALONE_SKILLS)
verify-snapshots просто запускал скрипт и помечал PASS — без
платформенной нагрузки. Опасный пробел: можно было закоммитить
snapshot, который 1С Designer не примет.

Теперь для skd-compile snapshot оборачивается во внешний отчёт
(erf-init --WithSKD), Template.xml подменяется на сгенерированный
кейсом, и запускается erf-build. Платформа парсит схему — если
принимает, кейс PASS; если отклоняет, в errors попадает её stderr.
Ссылочные типы (CatalogRef.X и т.п.) не требуют реальной базы:
epf-build сам поднимает временную stub-конфигурацию.

Если v8 недоступен — мягкий skip с пометкой "no v8 context".

Замер: 21 кейс x ~5s avg = ~110s на полный verify-snapshots
--skill skd-compile. Все 21 текущих кейса проходят — значит каждый
snapshot гарантированно платформо-валиден.

Аналогичная обёртка для mxl-compile / role-compile — отдельной
задачей по образцу.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:56:13 +03:00
Nick Shirokov 210bde7b2e docs(skd-compile): убрать список синонимов типов из раздела «Поля»
Перечень русских/альтернативных синонимов (`число`, `строка`,
`СправочникСсылка.X`, `int`, `bool` и т.п.) — справочный шум для
модели-пользователя: она по умолчанию пишет канонические английские
имена, а парсер тихо принимает синонимы через Resolve-TypeStr без
необходимости их декларировать в SKILL.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:38:34 +03:00
Nick Shirokov a932841418 fix(skd-info): убрать лишний пробел в 'Templates: N defined ( bindings)'
При схеме без field- и group-привязок строка вывода Templates выглядела
как 'Templates: 4 defined ( bindings)' — пустой блок с одиноким пробелом
перед 'bindings)'. Теперь, когда привязок нет, скобки опускаются:
'Templates: 4 defined'.

Версия v1.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:32:47 +03:00
Nick Shirokov 576f6dda8a feat(skd-compile): многоязычный title/presentation (объектная форма)
Везде, где DSL принимает title/presentation, теперь поддерживается
объектная форма с языками: "title": { "ru": "...", "en": "..." }.
Строка по-прежнему работает как ru-only.

Покрыто: field title, calculatedField title, parameter title/presentation,
settingsVariant title/presentation (root и в structure-items),
availableValue title/presentation, userSettingPresentation в filter/dataParameter,
mltext-значения в conditionalAppearance.appearance (ключи Текст/Заголовок/Формат).

Реализация:
- Хелпер emit_mltext / Emit-MLText расширен — принимает string|dict и
  итерирует по языкам.
- 8 inline-блоков LocalStringType в каждом скрипте заменены на вызовы
  хелпера (унификация — побочный эффект, бенефит на будущее).
- На входе сняты str()/"$()" коэрции для title/presentation, чтобы dict
  доходил до хелпера живым.

- SKILL.md: одна строка про объектную форму title.
- tests: новый snapshot-кейс multi-lang-title (5 узлов с ru+en).
- Версия v1.21.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:30:37 +03:00
Nick Shirokov 1a5b788305 feat(skd-compile): составной тип поля valueType (массив типов)
В объектной форме поля DataSet ключ "type" теперь принимает массив:
"type": ["CatalogRef.A", "CatalogRef.B"] — генерирует несколько <v8:Type>
внутри одного <valueType>. Типичный паттерн в ERP для полей-расшифровок
с составным ссылочным типом.

Shorthand остаётся одно-типовым (без перегрузки). Квалификаторы
((N)/(D,F)) применяются к каждому элементу массива независимо.

- skd-compile.ps1/py: Emit-ValueType/emit_value_type диспатчат на
  Emit-SingleValueType при строке или итерируют при массиве; field-парсер
  сохраняет массив, не приводя к строке. Версия v1.20.
- SKILL.md: один абзац после описания типов.
- tests: новый snapshot-кейс field-multi-type на 3 ссылочных типа.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:04:49 +03:00
Nick Shirokov b39da27d20 docs(skd-compile): описать presentationExpression и appearance на поле
В DSL skd-compile уже поддерживались ключи presentationExpression и
appearance в объектной форме поля DataSet, но в SKILL.md они не были
задокументированы — фичи существовали де-факто, но обнаружить их можно
было только чтением скрипта.

Заодно зафиксирован детерминизм порядка ключей appearance: PS5.1
hashtable не сохраняет порядок вставки, из-за чего PS- и PY-рантаймы
давали разный XML на одном входе. Заменено на [ordered]@{}.

- SKILL.md: новый блок «Дополнительные ключи объектной формы» в разделе «Поля»
- skd-compile.ps1/py: appearance = [ordered]@{} вместо @{}, версия v1.19
- tests: новый snapshot-кейс field-appearance-and-presentation,
  проходит на обоих рантаймах

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 17:57:04 +03:00
Nick Shirokov 1f23afe6f9 feat(skd-info): -Mode full перечисляет внешние наборы
Когда DataSetQuery нет, в секции query вместо безликого
"(no query datasets)" теперь печатается список objectName из
DataSetObject: "(no query datasets; external datasets: <names>)".
Не нужно скроллить вверх к Overview, чтобы увидеть источник схемы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:47:01 +03:00
Nick Shirokov eda7279de0 fix(skd-info): -Mode full на схеме без DataSetQuery
Show-Query/show_query при отсутствии DataSetQuery делал exit 1, что
обрывало full режим после Show-Overview — секции fields/resources/
params/variant пользователь не видел. Теперь в full проверяем наличие
Query-набора и при отсутствии печатаем "(no query datasets)" и
продолжаем. Прямой -Mode query сохраняет прежнее поведение.

Воспроизводилось на схемах-приёмниках с одним DataSetObject
(например, ЖурналОшибок в ERP).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:38:57 +03:00
Nick Shirokov b3c2602439 docs(skd-compile): добавить пример DataSetObject
Внешний набор данных (objectName) был упомянут одной фразой в
type-dispatch summary, без примера. Добавлен компактный JSON-пример
+ короткое объяснение как объект подключается к
ПроцессорКомпоновкиДанных.Инициализировать.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:32:54 +03:00
Nick Shirokov 853313faed feat(skills): тихий -Path алиас для input-параметров
Добавлен Alias('Path') / "-Path" к основному файловому параметру
в *-info, *-validate, *-edit, *-decompile (24 навыка × PS+PY).
Не документируется — fallback на случай если модель напишет -Path
вместо -TemplatePath/-FormPath/-ObjectPath/-SubsystemPath/-RightsPath/
-ConfigPath/-ExtensionPath/-CIPath. Поведение строго аддитивное.

Регресс: 336/336 PS, 336/336 PY.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:17:30 +03:00
Nick Shirokov b7fbede819 fix(tests): починить pre-existing фейлы integration и skd-* snapshots
- build-config/build-epf: заменить runtime-тип FormDataStructure на корректный *Object.XXX
- platform-cfe/config/epf: form-compile принимает -OutputPath (путь до Form.xml), не -FormPath
- skd-edit/info/validate: перегенерированы snapshots после feat(skd-compile) denyIncompleteValues=true (3729b63)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:48:23 +03:00
Nick Shirokov bdc38caffa refactor(form-add): объединить с epf-add-form, удалить специфичный навык
form-add теперь покрывает и объекты конфигурации, и standalone EPF/ERF
source tree (тип определяется из корневого XML, маппинг типов уже был).

Изменения form-add scaffold:
- Module.bsl: пустые регионы вместо скелета процедуры ПриСозданииНаСервере
- Form.xml: убран <Events> (раньше привязывал OnCreateAtServer к процедуре)
- Form.xml: <SavedData>true</SavedData> теперь условный — ставится для
  Catalog/Document/etc (стандарт ERP, 99% форм), не ставится для
  DataProcessor/Report/External* (где у объекта нет состояния)

Это согласуется с workflow: form-compile перегенерирует Form.xml целиком,
поэтому привязки в scaffold могут стать orphan; пустые регионы +
без Events — корректная стартовая точка, которую form-edit/form-compile
наполняют атомарно.

Удалён навык epf-add-form (директория + тесты), вызовы заменены на
form-add в integration-тестах, в кейсах epf-validate/help-add, в
description epf-init/epf-bsp-init, в docs и README.

Перегенерированы snapshot'ы 5 навыков (form-add, form-compile,
form-edit, form-info, form-validate). Платформенная верификация в 1С 8.3.24
прошла для всех 9 кейсов form-add.

Bump form-add v1.3 → v1.4.
2026-04-25 15:26:54 +03:00
Nick Shirokov 2a86df1c98 refactor(skills): унифицировать стиль триггеров в description
Заменено «пользователь просит» → «нужно» в 12 навыках для согласования
с доминирующим стилем репозитория (cf-*, cfe-*, form-*, skd-*, mxl-*,
role-*, interface-*, subsystem-edit/info/validate уже используют «нужно»).

Дополнительно у db-list переформулирован триггер: вместо «"добавь базу"»
(что коллидирует с db-create) — «зарегистрировать базу в реестре»,
точнее отражает суть скилла (управление .v8-project.json).

Затронуто: db-create, db-dump-cf, db-dump-xml, db-list, db-load-cf,
db-load-git, db-load-xml, db-run, db-update, meta-compile, meta-remove,
subsystem-compile.
2026-04-25 13:35:44 +03:00
Nick Shirokov 2e1487fd17 feat(skills): добавить триггерные фразы в description 8 навыков
Навыки, у которых description содержал только «что делает» без условия
«когда использовать»: epf-init, erf-init, form-add, template-add, epf-add-form,
epf-bsp-init, epf-bsp-add-command, img-grid.

Добавлено второе предложение в стиле репозитория («Используй когда нужно …»).
Для epf-bsp-* уточнено назначение через ключевые термины БСП
(СведенияОВнешнейОбработке, «Дополнительные отчёты и обработки»).

Co-authored-by: Serg2000Mr <129394542+Serg2000Mr@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:29:20 +03:00
Nick Shirokov 3729b63b89 feat(skd-compile): @autoDates — дефолты use=Always + denyIncompleteValues=true
Производные &НачалоПериода/&КонецПериода требуют заполненный период,
поэтому сам параметр теперь по умолчанию получает use=Always и
denyIncompleteValues=true. В объектной форме явные значения перекрывают.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:24:22 +03:00
Nick Shirokov c3ec51e174 feat(template-add): автопоиск SrcDir по стандартным подпапкам
Если <SrcDir>/<ObjectName>.xml не найден — сканирует Reports,
DataProcessors, Documents, Catalogs и другие папки типа объектов.
При 1 совпадении расширяет SrcDir, при нескольких — ошибка со списком.
Попутно — уточнение описания SrcDir, обезличенный пример, флаг
-SetMainSKD в PS-стиле.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:51:07 +03:00
Nick Shirokov fb58a04700 feat(web-test): getFormState — полные reportSettings + chip-значения
На DCS-формах возвращались только настройки с явным чекбоксом «Использование» — остальные (всегда включённые) отбрасывались и пропадали из fields[]. Reference-поля с chip-контролом возвращали пустое value, потому что значение живёт в .chipsItem .chipsTitle, а не в input.value.

- DCS-группировка больше не требует наличия «Использование»; при его отсутствии setting.enabled = true (настройка всегда активна)
- При чтении input.value делается fallback на .chipsItem .chipsTitle в LABEL-родителе — через запятую, если значений несколько (первый элемент + «+N» при свёртке в UI)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:18:48 +03:00
Nick Shirokov a30b720c89 docs(skd-compile): SKILL.md — упростить описание @autoDates и dataParameters auto
Убрать XML-детали (useRestriction, xsi:type, <use>false</use>, <value xsi:nil>);
описывать поведение с точки зрения автора СКД, а не внутреннего представления.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:17:16 +03:00
Nick Shirokov 54d47aadad feat(skd-compile): dataParameters auto — копирование value всех типов (ЕРП-паттерн)
Раньше "auto" копировал только variant для StandardPeriod, остальные типы
теряли значение по умолчанию. Теперь:

- value задан (не-Custom для StandardPeriod) → value + use=true (implicit),
  правильный xsi:type: boolean/decimal/dateTime/string, DesignTimeValue для
  ссылочных типов.
- value отсутствует или StandardPeriod=Custom → <use>false</use>
  + <value xsi:nil="true"/>.

Соответствует тому, как 1С Designer и ЕРП-отчёты персистят
SettingsParameterValue. Тест auto-data-parameters расширен покрытием
decimal/string/ref/nil.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:13:04 +03:00
Nick Shirokov 3f23be8219 feat(skd-compile): @autoDates — НачалоПериода/КонецПериода вместо ДатаНачала/ДатаОкончания
Канонический паттерн БСП в Титан/ЕРП-отчётах использует имена
НачалоПериода/КонецПериода (~10:1 по частоте). Выражения
&Период.ДатаНачала/&Период.ДатаОкончания сохранены — это обращение
к внутренним полям StandardPeriod.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:51:22 +03:00
Nick Shirokov f5dd677ac8 fix(tests): cf-info/config-with-objects — preRun через meta-compile
Тест сломался с 0d5d345 (ужесточение cf-edit add-childObject: теперь требует, чтобы файл объекта существовал на диске). Там были пофикшены 4 теста cf-edit, но этот кейс cf-info с тем же паттерном в preRun пропустили.

Заменил cf-edit add-childObject на три meta-compile (Catalog.Товары, Document.Заказ, Enum.Статусы) — те сами регистрируют объекты в Configuration.xml и создают файлы. Snapshot перегенерирован.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:08:37 +03:00
Nick Shirokov 02e9053d00 feat(cf-validate,cfe-validate,epf-validate): поддержка платформы 8.5 (issue #13)
- `CompatibilityMode`, `ConfigurationExtensionCompatibilityMode`: добавлен `Version8_5_1`
- `InterfaceCompatibilityMode`: расширен до полного списка из 7 значений (Version8_2, Version8_2EnableTaxi, Taxi, TaxiEnableVersion8_2, TaxiEnableVersion8_5, Version8_5EnableTaxi, Version8_5) — заодно учтены недостающие 8.2-значения
- Принимается `version="2.21"` в заголовке MetaDataObject
- cf-edit/reference.md: обновлена таблица допустимых значений

Genrators (form-compile, form-add, cfe-borrow и др.) уже подхватывают версию формата через Detect-FormatVersion — не трогаем.

Closes #13

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:08:27 +03:00
Nick Shirokov bd462f4cc3 fix(web-test): hasVisibleModal — handle duplicate #modalSurface
1С оставляет стейл-элемент #modalSurface (display:none) после закрытия
формы и создаёт второй при открытии новой модалки — в DOM оказывается два
элемента с одинаковым id. getElementById возвращал первый (скрытый), из-за
чего detectForm/detectForms не видели активную модалку: getFormState
выдавал form+buttons от родительской формы, а clickElement кликал мимо
или падал.

Сканируем все #modalSurface через querySelectorAll и берём первый с
offsetWidth > 0.

Воспроизводилось стабильно на СКД-расшифровке после открытия "Настройки..."
в форме отчёта.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 21:24:44 +03:00
Nick Shirokov be74d224be feat(skd-compile): dataParameters auto — наследовать variant для StandardPeriod
Для параметров типа StandardPeriod в режиме "dataParameters": "auto" эмитируется <dcscor:value> с variant из дефолта параметра (Custom, если не задан) — как это делает 1C Designer при сохранении SettingsParameterValue для периодов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 21:00:58 +03:00
Nick Shirokov 1b46eb4d85 feat(skd-compile): parameters — title и presentation как синонимы
- parameter принимает presentation как синоним title (1C UI показывает
  подпись параметра как "Представление" — модель по аналогии пишет presentation)
- availableValues[] принимает title как синоним presentation (обратная
  ошибка: модель пишет title по аналогии с самим параметром)

Обе формы пишутся в один и тот же XML-узел. Версии: skd-compile v1.13 → v1.14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 20:37:00 +03:00
Nick Shirokov 1cea9e794e feat(skd-compile,skd-edit): calculatedFields — shorthand и объектные синонимы
- skd-compile v1.13: Parse-CalcShorthand теперь понимает "[Title]:type=expr#flags"
  (синхронно со skd-edit). Emit-CalcFields принимает name как синоним
  field/dataPath и строковую форму useRestriction ("#noField #noFilter ...").
- skd-edit v1.11: #restrict парсится по known-names pattern — исключает ложные
  срабатывания на # внутри строковых литералов в выражении.

Закрывает три ловушки из upload/bug-skd-compile-calculated-field-datapath.md,
где модель писала name вместо field и строковый useRestriction по аналогии
с shorthand-флагами.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 20:31:14 +03:00
Nick Shirokov 00fafd4af5 fix(web-test): scanSpreadsheetCells — use contentFrame() instead of index-based frame mapping
Replace fragile page.frames()[iframeIdx + 1] with handle.contentFrame() for
reliable iframe-to-Playwright-Frame resolution. The old index arithmetic could
break when 1C web client accumulates extra frames during prolonged sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:15:05 +03:00
Nick Shirokov e30b518935 feat(skd-info): auto-resolve object directory path to DCS template
When model passes report/dataprocessor path instead of template path,
scan Templates/*.xml metadata for DataCompositionSchema type and
auto-resolve. Single match → resolve with [i] hint, multiple → list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:23:56 +03:00
Nick Shirokov 6d5c1a0b19 Merge branch 'dev' into feature/web-test-runner 2026-04-05 18:18:25 +03:00
Nick Shirokov b322c02fdb fix(web-test): discoverTests для одиночного файла + первый smoke-тест
- Fix: discoverTests падал с ENOTDIR при передаче .test.mjs файла
- Добавлен 01-navigation.test.mjs — навигация по разделам, открытие
  списков через navigateLink, переключение между подсистемами

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:22:40 +03:00
Nick Shirokov 61ef7ac891 fix(web-test): фикс синтетической конфигурации для загрузки в платформу
- Подсистемы: singular формы в Content (Catalog вместо Catalogs)
- КурсыВалют: Independent вместо RecorderSubordinate
- Убран AccumulationRegister (требует регистратор, не нужен для UI)
- Отчёт: запрос из ТЧ документа вместо регистра

Формы загружаются без Form.xml (автогенерация платформой) —
баг form-compile (XDTO exception) требует отдельного исследования.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:09:43 +03:00
Nick Shirokov ba19b4111d feat(web-test): синтетическая конфигурация для регресс-тестов
22 шага: cf-init → meta-compile (10 объектов) → form-compile (3 формы,
вкл. 2 вкладки для Номенклатуры) → skd-compile → subsystem-compile
(Склад + Администрирование) → role-compile (полные права) → cf-validate.

Расширения: иерархический справочник, разнотипные реквизиты (Number,
Boolean, Date, String unlimited), FillChecking, вторая подсистема.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:57:52 +03:00
Nick Shirokov ded11437c5 docs(web-test): обновить статус дорожной карты — #1-5 done
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:55:00 +03:00
Nick Shirokov 5eda7f8eb3 feat(web-test): test runner — buildContext, cmdTest, assertions, step
- Извлечён buildContext() из executeScript (переиспользуется)
- Новая команда `test [url] <dir> [--tags/--bail/--retry/--timeout/--report]`
- Обнаружение *.test.mjs, импорт ES-модулей, фильтрация по тегам/grep/only
- Хуки: prepare/cleanup (без браузера) + beforeAll/afterAll/beforeEach/afterEach
- Встроенный сброс состояния (dismissPendingErrors + closeForm) после каждого теста
- step(name, fn) обёртка с вложенностью и таймингами
- Assertions: ok/equal/deepEqual/includes/match/throws + 1C-специфичные
- Консольный вывод с деревом шагов, JSON-отчёт
- Поддержка webtest.config.mjs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:53:33 +03:00
Nick Shirokov f39a0d9c5e docs(web-test): BrowserContext вместо sequential reconnect для мульти-контекста
Один процесс браузера, несколько изолированных BrowserContext'ов.
Мгновенное переключение между пользователями, состояние каждой
сессии сохраняется. Не требует полного рефакторинга createContext().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:47:26 +03:00
Nick Shirokov 2347859bdd docs(web-test): спецификация test runner для регрессионного тестирования
Единый механизм для внутреннего регресса browser.mjs API и
пользовательского регресса 1С-приложений. Паттерны Playwright Test.

Содержание: CLI, формат тестов, контексты, хуки, assertions, step(),
отчёты (JSON/Allure/JUnit), синтетическая конфигурация, дорожная карта.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:37:47 +03:00
753 changed files with 24494 additions and 2307 deletions
+32
View File
@@ -0,0 +1,32 @@
{
"name": "cc-1c-skills",
"interface": {
"displayName": "1C Skills"
},
"plugins": [
{
"name": "1c-skills",
"source": {
"source": "url",
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
"ref": "port-codex"
},
"policy": {
"installation": "AVAILABLE"
},
"category": "Development"
},
{
"name": "1c-skills-py",
"source": {
"source": "url",
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
"ref": "port-codex-py"
},
"policy": {
"installation": "AVAILABLE"
},
"category": "Development"
}
]
}
+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/claude-code-marketplace-manifest.json",
"name": "cc-1c-skills",
"description": "Маркетплейс навыков для разработки на платформе 1С:Предприятие",
"owner": {
"name": "Nikolay Shirokov"
},
"plugins": [
{
"name": "1c-skills",
"source": "./",
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент."
},
{
"name": "1c-skills-py",
"source": {
"source": "github",
"repo": "Nikolay-Shirokov/cc-1c-skills",
"ref": "port-claude-code-py"
},
"description": "[Python] То же — для Linux/Mac или когда PowerShell недоступен."
}
]
}
+31
View File
@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "1c-skills",
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
"author": {
"name": "Nikolay Shirokov"
},
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"license": "MIT",
"keywords": [
"1c",
"1c-dev",
"cf",
"cfe",
"epf",
"erf",
"metadata",
"configuration",
"extension",
"form",
"report",
"skd",
"data-processor",
"mxl",
"web-client",
"testing",
"test-automation"
],
"skills": "./.claude/skills/"
}
+9 -7
View File
@@ -1,6 +1,6 @@
---
name: cf-edit
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
allowed-tools:
- Bash
@@ -24,7 +24,7 @@ allowed-tools:
| `NoValidate` | Пропустить авто-валидацию |
```powershell
powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
```
## Операции
@@ -37,6 +37,8 @@ powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -Conf
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
| `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) |
| `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) |
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
@@ -44,15 +46,15 @@ powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -Conf
```powershell
# Изменить версию и поставщика
... -ConfigPath test-tmp/cf -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
# Добавить объекты
... -ConfigPath test-tmp/cf -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
# Удалить объект
... -ConfigPath test-tmp/cf -Operation remove-childObject -Value "Catalog.Устаревший"
... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший"
# Роли по умолчанию
... -ConfigPath test-tmp/cf -Operation add-defaultRole -Value "ПолныеПрава"
... -ConfigPath test-tmp/cf -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава"
... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
```
+96 -13
View File
@@ -13,7 +13,7 @@
### Enum
| Свойство | Допустимые значения |
|----------|---------------------|
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_27`, `DontUse` |
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` |
| `ConfigurationExtensionCompatibilityMode` | то же |
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
| `ScriptVariant` | `Russian`, `English` |
@@ -21,7 +21,7 @@
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `InterfaceCompatibilityMode` | `Taxi`, `TaxiEnableVersion8_2`, `Version8_2` |
| `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` |
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
@@ -35,14 +35,7 @@
Формат: `Type.Name` — XML-тип и имя объекта через точку.
**Важно про `add-childObject`**: операция регистрирует в `<ChildObjects>` Configuration.xml только объект, **файл которого уже существует на диске** (например `Catalogs/Товары.xml`). Если файла нет — скрипт падает с exit 1 и подсказкой. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его в Configuration.xml за один вызов.
Когда `add-childObject` всё-таки нужен: откатили Configuration.xml (или перезаписали из выгрузки БД), а файлы объектов остались — нужно восстановить ссылки в `<ChildObjects>`.
При добавлении объект вставляется в каноническую позицию:
1. Находит последний элемент того же типа → вставляет после
2. Если тип отсутствует → находит последний элемент предшествующего типа → вставляет после
3. Внутри одного типа — алфавитный порядок
**Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов.
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
@@ -52,6 +45,99 @@ Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"
`set-defaultRoles` полностью заменяет список ролей.
## set-panels
Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране.
`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона).
**Запись** — одна из:
- Строка-алиас (одна панель в этом слоте)
- Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом)
**Алиасы панелей:**
| Алиас | Панель |
|-------|--------|
| `sections` | Панель разделов |
| `open` | Панель открытых |
| `favorites` | Панель избранного |
| `history` | Панель истории |
| `functions` | Панель функций текущего раздела |
**Семантика:**
- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов `<top>`/...)
- `{"group":[...]}` → один тег с `<group>`-обёрткой, элементы внутри идут стеком
**Пример** (DefinitionFile):
```json
[
{
"operation": "set-panels",
"value": {
"top": ["open"],
"left": ["sections"],
"right": [{ "group": ["favorites", "history"] }],
"bottom": ["functions"]
}
}
]
```
Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`.
## set-home-page
Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует.
`value` — объект:
| Ключ | Канонич. (XML) | Описание |
|------|----------------|----------|
| `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` |
| `left` | `LeftColumn` | массив записей форм |
| `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) |
Принимаются и короткие и канонич. ключи (XML-имена) — оба работают.
**Запись формы** — одна из:
- Строка `"<form>"` — только имя формы, дефолты `height=10`, `visibility=true`
- Объект `{form, height?, visibility?, roles?}`
| Поле | Канонич. | Дефолт | Описание |
|------|----------|--------|----------|
| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) |
| `height` | `Height` | `10` | Высота |
| `visibility` | `Visibility` | `true` | Общая видимость (`<xr:Common>`) |
| `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям |
**Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`.
**Пример:**
```json
[
{
"operation": "set-home-page",
"value": {
"template": "TwoColumnsVariableWidth",
"left": [
"CommonForm.НачалоРаботы",
{ "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false },
{ "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 },
{
"form": "CommonForm.РабочийСтолОператора",
"visibility": false,
"roles": { "Role.Оператор": true, "Role.ПолныеПрава": false }
}
],
"right": [
{ "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 }
]
}
}
]
```
## DefinitionFile (JSON)
```json
@@ -62,6 +148,3 @@ Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"
]
```
## Авто-валидация
После сохранения автоматически запускается `cf-validate` (если не указан `-NoValidate`).
+316 -10
View File
@@ -1,9 +1,9 @@
# cf-edit v1.1 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.4 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$ConfigPath,
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
[string]$DefinitionFile,
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles")]
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page")]
[string]$Operation,
[string]$Value,
[switch]$NoValidate
@@ -444,6 +444,308 @@ function Do-RemoveDefaultRole([string]$batchVal) {
}
}
# --- Operation: set-panels ---
# Canonical English aliases — preferred form, used in docs and error messages.
$script:panelUuids = @{
"sections" = "b553047f-c9aa-4157-978d-448ecad24248"
"open" = "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"
"favorites" = "13322b22-3960-4d68-93a6-fe2dd7f28ca3"
"history" = "c933ac92-92cd-459d-81cc-e0c8a83ced99"
"functions" = "b2735bd3-d822-4430-ba59-c9e869693b24"
}
# Russian synonyms — silently accepted (cf-info displays Russian names; users
# may copy them straight into cf-edit value).
$script:panelSynonyms = @{
"разделов" = "sections"; "разделы" = "sections"
"открытых" = "open"; "открытые" = "open"
"избранного" = "favorites";"избранное" = "favorites"
"истории" = "history"; "история" = "history"
"функций" = "functions";"функции" = "functions"
}
function Build-PanelEntryXml($entry, [string]$indent) {
# String alias -> <panel><uuid>...</uuid></panel>
if ($entry -is [string]) {
$key = $entry.ToLowerInvariant()
if ($script:panelSynonyms.ContainsKey($key)) { $key = $script:panelSynonyms[$key] }
if (-not $script:panelUuids.ContainsKey($key)) {
Write-Error "Unknown panel alias '$entry'. Allowed: $(($script:panelUuids.Keys | Sort-Object) -join ', ')"
exit 1
}
$u = $script:panelUuids[$key]
$instId = [guid]::NewGuid().ToString()
return "$indent<panel id=`"$instId`">`r`n$indent`t<uuid>$u</uuid>`r`n$indent</panel>"
}
# Object {group: [...]} -> <group id=""><group><panel/></group>...</group> (stack)
if ($entry.PSObject.Properties['group']) {
$children = $entry.group
if (-not $children -or $children.Count -eq 0) {
Write-Error "group must contain at least one entry"
exit 1
}
$gid = [guid]::NewGuid().ToString()
$inner = ""
foreach ($child in $children) {
$childXml = Build-PanelEntryXml $child "$indent`t`t"
$inner += "$indent`t<group>`r`n$childXml`r`n$indent`t</group>`r`n"
}
return "$indent<group id=`"$gid`">`r`n$inner$indent</group>"
}
Write-Error "Panel entry must be a string alias or object {group:[...]}, got: $($entry | ConvertTo-Json -Compress)"
exit 1
}
function Do-SetPanels($valArg) {
# Accept string (JSON), PSCustomObject, or hashtable
$layout = $valArg
if ($layout -is [string]) {
try { $layout = $layout | ConvertFrom-Json } catch {
Write-Error "set-panels value must be valid JSON object, got: $valArg"
exit 1
}
}
if (-not $layout) {
Write-Error "set-panels value is empty"
exit 1
}
$sides = @("top","left","right","bottom")
$bodyParts = @()
foreach ($side in $sides) {
$entries = $null
if ($layout.PSObject.Properties[$side]) { $entries = $layout.$side }
if ($null -eq $entries) { continue }
# Normalize to array
if ($entries -isnot [System.Array] -and $entries -isnot [System.Collections.IList]) {
$entries = @($entries)
}
foreach ($entry in $entries) {
$entryXml = Build-PanelEntryXml $entry "`t`t"
$bodyParts += "`t<$side>`r`n$entryXml`r`n`t</$side>"
}
}
# Reject unknown side keys (catches typos like "Top" vs "top")
foreach ($prop in $layout.PSObject.Properties) {
if ($sides -notcontains $prop.Name) {
Write-Error "Unknown side '$($prop.Name)'. Allowed: $($sides -join ', ')"
exit 1
}
}
$body = $bodyParts -join "`r`n"
$declarations = @"
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
"@
$bodyBlock = if ($body) { "$body`r`n" } else { "" }
$caiXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
$bodyBlock$declarations
</ClientApplicationInterface>
"@
$extDir = Join-Path $script:configDir "Ext"
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
$caiPath = Join-Path $extDir "ClientApplicationInterface.xml"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($caiPath, $caiXml, $utf8Bom)
$script:modifyCount++
Info "Wrote panel layout: $caiPath"
}
# --- Operation: set-home-page ---
# Russian → English type aliases for form-ref normalization
$script:ruTypeMap = @{
"справочник" = "Catalog"
"документ" = "Document"
"перечисление" = "Enum"
"отчёт" = "Report"
"отчет" = "Report"
"обработка" = "DataProcessor"
"общаяформа" = "CommonForm"
"журналдокументов" = "DocumentJournal"
"планвидовхарактеристик" = "ChartOfCharacteristicTypes"
"плансчетов" = "ChartOfAccounts"
"планвидоврасчета" = "ChartOfCalculationTypes"
"планвидоврасчёта" = "ChartOfCalculationTypes"
"регистрсведений" = "InformationRegister"
"регистрнакопления" = "AccumulationRegister"
"регистрбухгалтерии" = "AccountingRegister"
"регистррасчета" = "CalculationRegister"
"регистррасчёта" = "CalculationRegister"
"бизнеспроцесс" = "BusinessProcess"
"задача" = "Task"
"планобмена" = "ExchangePlan"
"хранилищенастроек" = "SettingsStorage"
}
# plural folder → singular type
$script:dirToType = @{}
foreach ($k in $script:typeToDir.Keys) { $script:dirToType[$script:typeToDir[$k].ToLowerInvariant()] = $k }
function Normalize-FormRef([string]$s) {
$s = $s.Trim()
if (-not $s) { return $s }
# UUID — leave as-is
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}$') { return $s }
# Path form?
if ($s.Contains("/") -or $s.Contains("\")) {
$parts = $s.Replace("\","/").Split("/") | Where-Object { $_ -ne "" -and $_.ToLowerInvariant() -ne "ext" }
# Strip trailing Form.xml
if ($parts.Count -gt 0 -and $parts[-1].ToLowerInvariant() -eq "form.xml") {
$parts = @($parts[0..($parts.Count - 2)])
}
if ($parts.Count -ge 2) {
$typeDir = $parts[0]
$typeSingular = $script:dirToType[$typeDir.ToLowerInvariant()]
if ($typeSingular) {
if ($typeSingular -eq "CommonForm" -and $parts.Count -ge 2) {
return "CommonForm.$($parts[1])"
}
if ($parts.Count -ge 4 -and $parts[2].ToLowerInvariant() -eq "forms") {
return "$typeSingular.$($parts[1]).Form.$($parts[3])"
}
}
}
return $s
}
# Dot form — translate Russian head and 'Форма' segment, auto-insert 'Form'
$segs = $s.Split(".")
if ($segs.Count -ge 1) {
$head = $segs[0].ToLowerInvariant()
if ($script:ruTypeMap.ContainsKey($head)) { $segs[0] = $script:ruTypeMap[$head] }
for ($i = 1; $i -lt $segs.Count; $i++) {
if ($segs[$i] -eq "Форма") { $segs[$i] = "Form" }
}
# Auto-insert Form: for object types with 3 segments (Type.Object.FormName)
if ($segs.Count -eq 3 -and $script:typeOrder -contains $segs[0] -and $segs[0] -ne "CommonForm") {
$segs = @($segs[0], $segs[1], "Form", $segs[2])
}
}
return ($segs -join ".")
}
# Accept short DSL or canonical XML keys (silently)
function Get-FieldValue($obj, [string[]]$keys) {
foreach ($k in $keys) {
if ($obj.PSObject.Properties[$k]) { return $obj.PSObject.Properties[$k].Value }
}
return $null
}
function Build-HomePageItemXml($entry, [string]$indent) {
# Resolve fields
if ($entry -is [string]) {
$formRef = Normalize-FormRef $entry
$height = 10
$common = $true
$roles = $null
} else {
$formRaw = Get-FieldValue $entry @("form","Form")
if (-not $formRaw) { Write-Error "Home page item: 'form' is required, got: $($entry | ConvertTo-Json -Compress)"; exit 1 }
$formRef = Normalize-FormRef ([string]$formRaw)
$h = Get-FieldValue $entry @("height","Height")
$height = if ($null -ne $h) { [int]$h } else { 10 }
$vis = Get-FieldValue $entry @("visibility","Visibility")
$common = if ($null -ne $vis) { [bool]$vis } else { $true }
$roles = Get-FieldValue $entry @("roles")
}
$visParts = @()
$visParts += "$indent`t`t<xr:Common>$($common.ToString().ToLower())</xr:Common>"
if ($roles) {
# roles is PSCustomObject {Role.X: bool, ...}
foreach ($prop in $roles.PSObject.Properties) {
$rname = $prop.Name
if (-not $rname.StartsWith("Role.") -and -not ($rname -match '^[0-9a-fA-F]{8}-')) { $rname = "Role.$rname" }
$rval = ([bool]$prop.Value).ToString().ToLower()
$escName = [System.Security.SecurityElement]::Escape($rname)
$visParts += "$indent`t`t<xr:Value name=`"$escName`">$rval</xr:Value>"
}
}
$visBlock = $visParts -join "`r`n"
$escForm = [System.Security.SecurityElement]::Escape($formRef)
return @"
$indent<Item>
$indent`t<Form>$escForm</Form>
$indent`t<Height>$height</Height>
$indent`t<Visibility>
$visBlock
$indent`t</Visibility>
$indent</Item>
"@
}
function Do-SetHomePage($valArg) {
$layout = $valArg
if ($layout -is [string]) {
try { $layout = $layout | ConvertFrom-Json } catch {
Write-Error "set-home-page value must be valid JSON object"; exit 1
}
}
if (-not $layout) { Write-Error "set-home-page value is empty"; exit 1 }
$allowedTemplates = @("OneColumn","TwoColumnsEqualWidth","TwoColumnsVariableWidth")
$tmpl = Get-FieldValue $layout @("template","WorkingAreaTemplate")
if (-not $tmpl) { $tmpl = "TwoColumnsEqualWidth" }
if ($allowedTemplates -notcontains $tmpl) {
Write-Error "Unknown template '$tmpl'. Allowed: $($allowedTemplates -join ', ')"; exit 1
}
$leftItems = Get-FieldValue $layout @("left","LeftColumn")
$rightItems = Get-FieldValue $layout @("right","RightColumn")
# Reject unknown keys
$known = @("template","WorkingAreaTemplate","left","LeftColumn","right","RightColumn")
foreach ($prop in $layout.PSObject.Properties) {
if ($known -notcontains $prop.Name) {
Write-Error "Unknown key '$($prop.Name)'. Allowed: template, left, right"; exit 1
}
}
if ($tmpl -eq "OneColumn" -and $rightItems) {
Write-Error "Template 'OneColumn' cannot have items in 'right' column"; exit 1
}
function Build-Column([string]$tag, $items) {
if (-not $items) { return "`t<$tag/>" }
if ($items -isnot [System.Array] -and $items -isnot [System.Collections.IList]) {
$items = @($items)
}
if ($items.Count -eq 0) { return "`t<$tag/>" }
$itemBlocks = @()
foreach ($it in $items) {
$itemBlocks += Build-HomePageItemXml $it "`t`t"
}
$body = $itemBlocks -join "`r`n"
return "`t<$tag>`r`n$body`r`n`t</$tag>"
}
$leftXml = Build-Column "LeftColumn" $leftItems
$rightXml = Build-Column "RightColumn" $rightItems
$hpXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<WorkingAreaTemplate>$tmpl</WorkingAreaTemplate>
$leftXml
$rightXml
</HomePageWorkArea>
"@
$extDir = Join-Path $script:configDir "Ext"
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
$hpPath = Join-Path $extDir "HomePageWorkArea.xml"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($hpPath, $hpXml, $utf8Bom)
$script:modifyCount++
Info "Wrote home page layout: $hpPath"
}
# --- Operation: set-defaultRoles ---
function Do-SetDefaultRoles([string]$batchVal) {
$items = Parse-BatchValue $batchVal
@@ -508,15 +810,19 @@ if ($DefinitionFile) {
foreach ($op in $operations) {
$opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" }
$opValue = if ($op.value) { "$($op.value)" } else { "$Value" }
# Pass value through as-is (object or string); set-panels needs object form
$opValue = if ($null -ne $op.value) { $op.value } else { $Value }
$opValueStr = if ($opValue -is [string]) { $opValue } else { "$opValue" }
switch ($opName) {
"modify-property" { Do-ModifyProperty $opValue }
"add-childObject" { Do-AddChildObject $opValue }
"remove-childObject" { Do-RemoveChildObject $opValue }
"add-defaultRole" { Do-AddDefaultRole $opValue }
"remove-defaultRole" { Do-RemoveDefaultRole $opValue }
"set-defaultRoles" { Do-SetDefaultRoles $opValue }
"modify-property" { Do-ModifyProperty $opValueStr }
"add-childObject" { Do-AddChildObject $opValueStr }
"remove-childObject" { Do-RemoveChildObject $opValueStr }
"add-defaultRole" { Do-AddDefaultRole $opValueStr }
"remove-defaultRole" { Do-RemoveDefaultRole $opValueStr }
"set-defaultRoles" { Do-SetDefaultRoles $opValueStr }
"set-panels" { Do-SetPanels $opValue }
"set-home-page" { Do-SetHomePage $opValue }
default { Write-Error "Unknown operation: $opName"; exit 1 }
}
}
+278 -10
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-edit v1.1 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.4 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -7,6 +7,7 @@ import json
import os
import subprocess
import sys
import uuid as _uuid
from html import escape as html_escape
from lxml import etree
@@ -159,9 +160,9 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
parser.add_argument("-ConfigPath", required=True)
parser.add_argument("-ConfigPath", "-Path", required=True)
parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles"])
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
parser.add_argument("-Value", default=None)
parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args()
@@ -493,6 +494,269 @@ def main():
modify_count += 1
info(f"Set DefaultRoles: {len(items)} roles")
# --- set-panels (writes Ext/ClientApplicationInterface.xml from scratch) ---
# Canonical English aliases — preferred form, used in docs and error messages.
PANEL_UUIDS = {
"sections": "b553047f-c9aa-4157-978d-448ecad24248",
"open": "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75",
"favorites": "13322b22-3960-4d68-93a6-fe2dd7f28ca3",
"history": "c933ac92-92cd-459d-81cc-e0c8a83ced99",
"functions": "b2735bd3-d822-4430-ba59-c9e869693b24",
}
# Russian synonyms — silently accepted (cf-info displays Russian names;
# users may copy them straight into cf-edit value).
PANEL_SYNONYMS = {
"разделов": "sections", "разделы": "sections",
"открытых": "open", "открытые": "open",
"избранного": "favorites","избранное": "favorites",
"истории": "history", "история": "history",
"функций": "functions", "функции": "functions",
}
def build_panel_entry_xml(entry, indent):
if isinstance(entry, str):
key = entry.lower()
key = PANEL_SYNONYMS.get(key, key)
if key not in PANEL_UUIDS:
allowed = ", ".join(sorted(PANEL_UUIDS.keys()))
print(f"Unknown panel alias '{entry}'. Allowed: {allowed}", file=sys.stderr)
sys.exit(1)
inst = str(_uuid.uuid4())
return f'{indent}<panel id="{inst}">\r\n{indent}\t<uuid>{PANEL_UUIDS[key]}</uuid>\r\n{indent}</panel>'
if isinstance(entry, dict) and "group" in entry:
children = entry["group"]
if not children:
print("group must contain at least one entry", file=sys.stderr)
sys.exit(1)
gid = str(_uuid.uuid4())
inner = ""
for child in children:
child_xml = build_panel_entry_xml(child, indent + "\t\t")
inner += f"{indent}\t<group>\r\n{child_xml}\r\n{indent}\t</group>\r\n"
return f'{indent}<group id="{gid}">\r\n{inner}{indent}</group>'
print(f"Panel entry must be string alias or {{group:[...]}}, got: {entry!r}", file=sys.stderr)
sys.exit(1)
def do_set_panels(value):
nonlocal modify_count
layout = value
if isinstance(layout, str):
try:
layout = json.loads(layout)
except json.JSONDecodeError:
print(f"set-panels value must be valid JSON object", file=sys.stderr)
sys.exit(1)
if not isinstance(layout, dict) or not layout:
print("set-panels value must be non-empty object", file=sys.stderr)
sys.exit(1)
sides = ("top", "left", "right", "bottom")
# Reject unknown side keys
for k in layout.keys():
if k not in sides:
print(f"Unknown side '{k}'. Allowed: {', '.join(sides)}", file=sys.stderr)
sys.exit(1)
body_parts = []
for side in sides:
entries = layout.get(side)
if entries is None:
continue
if not isinstance(entries, list):
entries = [entries]
for entry in entries:
entry_xml = build_panel_entry_xml(entry, "\t\t")
body_parts.append(f"\t<{side}>\r\n{entry_xml}\r\n\t</{side}>")
body = "\r\n".join(body_parts)
body_block = body + "\r\n" if body else ""
declarations = (
'\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>\r\n'
'\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>\r\n'
'\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>\r\n'
'\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>\r\n'
'\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>'
)
cai_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\r\n'
'<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" '
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
'xsi:type="InterfaceLayouter">\r\n'
f'{body_block}{declarations}\r\n'
'</ClientApplicationInterface>'
)
ext_dir = os.path.join(config_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
cai_path = os.path.join(ext_dir, "ClientApplicationInterface.xml")
with open(cai_path, "w", encoding="utf-8-sig", newline="") as fh:
fh.write(cai_xml)
modify_count += 1
info(f"Wrote panel layout: {cai_path}")
# --- set-home-page (writes Ext/HomePageWorkArea.xml from scratch) ---
RU_TYPE_MAP = {
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
"плансчетов": "ChartOfAccounts",
"планвидоврасчета": "ChartOfCalculationTypes",
"планвидоврасчёта": "ChartOfCalculationTypes",
"регистрсведений": "InformationRegister",
"регистрнакопления": "AccumulationRegister",
"регистрбухгалтерии": "AccountingRegister",
"регистррасчета": "CalculationRegister",
"регистррасчёта": "CalculationRegister",
"бизнеспроцесс": "BusinessProcess",
"задача": "Task", "планобмена": "ExchangePlan",
"хранилищенастроек": "SettingsStorage",
}
DIR_TO_TYPE = {v.lower(): k for k, v in TYPE_TO_DIR.items()}
UUID_RE = __import__("re").compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
def normalize_form_ref(s):
s = (s or "").strip()
if not s:
return s
if UUID_RE.match(s):
return s
if "/" in s or "\\" in s:
parts = [p for p in s.replace("\\", "/").split("/") if p and p.lower() != "ext"]
if parts and parts[-1].lower() == "form.xml":
parts = parts[:-1]
if len(parts) >= 2:
type_dir = parts[0]
type_singular = DIR_TO_TYPE.get(type_dir.lower())
if type_singular:
if type_singular == "CommonForm" and len(parts) >= 2:
return f"CommonForm.{parts[1]}"
if len(parts) >= 4 and parts[2].lower() == "forms":
return f"{type_singular}.{parts[1]}.Form.{parts[3]}"
return s
segs = s.split(".")
if segs:
head = segs[0].lower()
if head in RU_TYPE_MAP:
segs[0] = RU_TYPE_MAP[head]
for i in range(1, len(segs)):
if segs[i] == "Форма":
segs[i] = "Form"
if len(segs) == 3 and segs[0] in TYPE_ORDER and segs[0] != "CommonForm":
segs = [segs[0], segs[1], "Form", segs[2]]
return ".".join(segs)
def get_field(obj, keys):
for k in keys:
if isinstance(obj, dict) and k in obj:
return obj[k]
return None
def build_home_page_item_xml(entry, indent):
if isinstance(entry, str):
form_ref = normalize_form_ref(entry)
height = 10
common = True
roles = None
elif isinstance(entry, dict):
form_raw = get_field(entry, ["form", "Form"])
if not form_raw:
print(f"Home page item: 'form' is required, got: {entry!r}", file=sys.stderr)
sys.exit(1)
form_ref = normalize_form_ref(str(form_raw))
h = get_field(entry, ["height", "Height"])
height = int(h) if h is not None else 10
vis = get_field(entry, ["visibility", "Visibility"])
common = bool(vis) if vis is not None else True
roles = get_field(entry, ["roles"])
else:
print(f"Home page item must be string or object, got: {entry!r}", file=sys.stderr)
sys.exit(1)
vis_parts = [f"{indent}\t\t<xr:Common>{str(common).lower()}</xr:Common>"]
if roles and isinstance(roles, dict):
for rname, rval in roles.items():
if not rname.startswith("Role.") and not UUID_RE.match(rname):
rname = f"Role.{rname}"
rval_s = str(bool(rval)).lower()
vis_parts.append(f'{indent}\t\t<xr:Value name="{html_escape(rname, quote=True)}">{rval_s}</xr:Value>')
vis_block = "\r\n".join(vis_parts)
esc_form = html_escape(form_ref, quote=True)
return (
f"{indent}<Item>\r\n"
f"{indent}\t<Form>{esc_form}</Form>\r\n"
f"{indent}\t<Height>{height}</Height>\r\n"
f"{indent}\t<Visibility>\r\n"
f"{vis_block}\r\n"
f"{indent}\t</Visibility>\r\n"
f"{indent}</Item>"
)
def do_set_home_page(value):
nonlocal modify_count
layout = value
if isinstance(layout, str):
try:
layout = json.loads(layout)
except json.JSONDecodeError:
print("set-home-page value must be valid JSON object", file=sys.stderr)
sys.exit(1)
if not isinstance(layout, dict) or not layout:
print("set-home-page value must be non-empty object", file=sys.stderr)
sys.exit(1)
allowed_templates = ("OneColumn", "TwoColumnsEqualWidth", "TwoColumnsVariableWidth")
tmpl = get_field(layout, ["template", "WorkingAreaTemplate"]) or "TwoColumnsEqualWidth"
if tmpl not in allowed_templates:
print(f"Unknown template '{tmpl}'. Allowed: {', '.join(allowed_templates)}", file=sys.stderr)
sys.exit(1)
left_items = get_field(layout, ["left", "LeftColumn"])
right_items = get_field(layout, ["right", "RightColumn"])
known = {"template", "WorkingAreaTemplate", "left", "LeftColumn", "right", "RightColumn"}
for k in layout.keys():
if k not in known:
print(f"Unknown key '{k}'. Allowed: template, left, right", file=sys.stderr)
sys.exit(1)
if tmpl == "OneColumn" and right_items:
print("Template 'OneColumn' cannot have items in 'right' column", file=sys.stderr)
sys.exit(1)
def build_column(tag, items):
if not items:
return f"\t<{tag}/>"
if not isinstance(items, list):
items = [items]
if not items:
return f"\t<{tag}/>"
blocks = [build_home_page_item_xml(it, "\t\t") for it in items]
body = "\r\n".join(blocks)
return f"\t<{tag}>\r\n{body}\r\n\t</{tag}>"
left_xml = build_column("LeftColumn", left_items)
right_xml = build_column("RightColumn", right_items)
hp_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\r\n'
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">\r\n'
f'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
f'{left_xml}\r\n'
f'{right_xml}\r\n'
'</HomePageWorkArea>'
)
ext_dir = os.path.join(config_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
hp_path = os.path.join(ext_dir, "HomePageWorkArea.xml")
with open(hp_path, "w", encoding="utf-8-sig", newline="") as fh:
fh.write(hp_xml)
modify_count += 1
info(f"Wrote home page layout: {hp_path}")
# --- Execute operations ---
operations = []
if args.DefinitionFile:
@@ -513,17 +777,21 @@ def main():
op_value = op.get("value", args.Value or "")
if op_name == "modify-property":
do_modify_property(op_value)
do_modify_property(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "add-childObject":
do_add_child_object(op_value)
do_add_child_object(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "remove-childObject":
do_remove_child_object(op_value)
do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "add-defaultRole":
do_add_default_role(op_value)
do_add_default_role(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "remove-defaultRole":
do_remove_default_role(op_value)
do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "set-defaultRoles":
do_set_default_roles(op_value)
do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "set-panels":
do_set_panels(op_value)
elif op_name == "set-home-page":
do_set_home_page(op_value)
else:
print(f"Unknown operation: {op_name}", file=sys.stderr)
sys.exit(1)
@@ -538,7 +806,7 @@ def main():
if os.path.isfile(validate_script):
print()
print("--- Running cf-validate ---")
subprocess.run([sys.executable, validate_script, "-ConfigPath", resolved_path])
subprocess.run([sys.executable, validate_script, "-ConfigPath", "-Path", resolved_path])
# --- Summary ---
print()
+10 -6
View File
@@ -1,7 +1,7 @@
---
name: cf-info
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
argument-hint: <ConfigPath> [-Mode overview|brief|full]
argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page]
allowed-tools:
- Bash
- Read
@@ -18,11 +18,12 @@ allowed-tools:
|----------|----------|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
| `Mode` | Режим: `overview` (default), `brief`, `full` |
| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` |
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell
powershell.exe -NoProfile -File .claude/skills/cf-info/scripts/cf-info.ps1 -ConfigPath "<путь>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>"
```
## Три режима
@@ -37,14 +38,17 @@ powershell.exe -NoProfile -File .claude/skills/cf-info/scripts/cf-info.ps1 -Conf
```powershell
# Обзор пустой конфигурации
... -ConfigPath upload/cfempty
... -ConfigPath src
# Краткая сводка реальной конфигурации
... -ConfigPath upload/acc_8.3.24 -Mode brief
... -ConfigPath src -Mode brief
# Полная информация
... -ConfigPath upload/acc_8.3.24 -Mode full
... -ConfigPath src -Mode full
# С пагинацией
... -ConfigPath upload/acc_8.3.24 -Mode full -Limit 50 -Offset 100
... -ConfigPath src -Mode full -Limit 50 -Offset 100
# Drill-down: только начальная страница (раскладка форм с ролями)
... -ConfigPath src -Section home-page
```
+197 -5
View File
@@ -1,9 +1,12 @@
# cf-info v1.0 — Compact summary of 1C configuration root
# cf-info v1.2 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][string]$ConfigPath,
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
[ValidateSet("overview","brief","full")]
[string]$Mode = "overview",
[Alias('Name')]
[ValidateSet("home-page")]
[string]$Section,
[int]$Limit = 150,
[int]$Offset = 0,
[string]$OutFile
@@ -118,6 +121,115 @@ $typeRuNames = @{
"Task"="Задачи"; "IntegrationService"="Сервисы интеграции"
}
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
$script:panelNames = @{
"cbab57f2-a0f3-4f0a-89ea-4cb19570ab75" = "Открытых"
"b553047f-c9aa-4157-978d-448ecad24248" = "Разделов"
"13322b22-3960-4d68-93a6-fe2dd7f28ca3" = "Избранного"
"c933ac92-92cd-459d-81cc-e0c8a83ced99" = "История"
"b2735bd3-d822-4430-ba59-c9e869693b24" = "Функций"
}
function Get-PanelsLayout {
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
$caiPath = Join-Path (Join-Path $configDir "Ext") "ClientApplicationInterface.xml"
if (-not (Test-Path $caiPath)) { return $null }
try { [xml]$caiDoc = Get-Content -Path $caiPath -Encoding UTF8 } catch { return $null }
if (-not $caiDoc.DocumentElement) { return $null }
$caiNs = New-Object System.Xml.XmlNamespaceManager($caiDoc.NameTable)
$caiNs.AddNamespace("ca", "http://v8.1c.ru/8.2/managed-application/core")
$layout = [ordered]@{ top=@(); left=@(); right=@(); bottom=@(); declared=@() }
foreach ($side in @("top","left","right","bottom")) {
foreach ($sideEl in $caiDoc.DocumentElement.SelectNodes("ca:$side", $caiNs)) {
$slot = @()
foreach ($u in $sideEl.SelectNodes(".//ca:panel/ca:uuid", $caiNs)) {
$key = $u.InnerText.Trim()
$nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" }
$slot += $nm
}
if ($slot.Count -gt 0) { $layout[$side] += ,$slot }
}
}
foreach ($pd in $caiDoc.DocumentElement.SelectNodes("ca:panelDef", $caiNs)) {
$key = $pd.GetAttribute("id")
$nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" }
$layout.declared += $nm
}
return $layout
}
function Format-LayoutSlots($slots) {
# slots is array of arrays (each inner array = one side-tag's panels, may be 1+)
# Single inner array, single panel -> just name
# Single inner array, multiple panels -> "Стек(a, b)"
# Multiple inner arrays -> separate entries joined by " | "
if (-not $slots -or $slots.Count -eq 0) { return "" }
$parts = @()
foreach ($slot in $slots) {
if ($slot.Count -eq 1) { $parts += $slot[0] }
else { $parts += ("Стек(" + ($slot -join ", ") + ")") }
}
return ($parts -join " | ")
}
$script:panelLayout = Get-PanelsLayout
# --- Read home page layout (Ext/HomePageWorkArea.xml) ---
function Get-HomePageLayout {
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
$hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml"
if (-not (Test-Path $hpPath)) { return $null }
try { [xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8 } catch { return $null }
if (-not $hpDoc.DocumentElement) { return $null }
$hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable)
$hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops")
$hpNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
$result = [ordered]@{ template = ""; left = @(); right = @() }
$tmplNode = $hpDoc.DocumentElement.SelectSingleNode("hp:WorkingAreaTemplate", $hpNs)
if ($tmplNode) { $result.template = $tmplNode.InnerText.Trim() }
foreach ($colName in @("LeftColumn","RightColumn")) {
$colNode = $hpDoc.DocumentElement.SelectSingleNode("hp:$colName", $hpNs)
if (-not $colNode) { continue }
$items = @()
foreach ($item in $colNode.SelectNodes("hp:Item", $hpNs)) {
$f = $item.SelectSingleNode("hp:Form", $hpNs)
$h = $item.SelectSingleNode("hp:Height", $hpNs)
$visNode = $item.SelectSingleNode("hp:Visibility", $hpNs)
$common = $true
$roles = @()
if ($visNode) {
$cn = $visNode.SelectSingleNode("xr:Common", $hpNs)
if ($cn) { $common = ($cn.InnerText.Trim() -eq "true") }
foreach ($v in $visNode.SelectNodes("xr:Value", $hpNs)) {
$roles += @{ name = $v.GetAttribute("name"); value = ($v.InnerText.Trim() -eq "true") }
}
}
$items += [ordered]@{
form = if ($f) { $f.InnerText.Trim() } else { "" }
height = if ($h) { [int]$h.InnerText.Trim() } else { 10 }
common = $common
roles = $roles
}
}
if ($colName -eq "LeftColumn") { $result.left = $items } else { $result.right = $items }
}
return $result
}
$script:homePage = Get-HomePageLayout
function Format-HomePageItem($it, [bool]$detailed) {
$badges = @()
$badges += "h=$($it.height)"
if (-not $it.common) { $badges += "скрыта" }
if ($it.roles.Count -gt 0) {
if ($detailed) { $badges += "роли: $($it.roles.Count)" }
else { $badges += "+$($it.roles.Count) ролей" }
}
$tail = if ($badges.Count -gt 0) { " (" + ($badges -join ", ") + ")" } else { "" }
return " $($it.form)$tail"
}
# --- Count objects in ChildObjects ---
$objectCounts = [ordered]@{}
$totalObjects = 0
@@ -154,7 +266,7 @@ $cfgDbSpaces = Get-PropText "DatabaseTablespacesUseMode"
$cfgWindowMode = Get-PropText "MainClientApplicationWindowMode"
# --- BRIEF mode ---
if ($Mode -eq "brief") {
if ($Mode -eq "brief" -and -not $Section) {
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
$compatPart = if ($cfgCompat) { " | $cfgCompat" } else { "" }
@@ -162,7 +274,7 @@ if ($Mode -eq "brief") {
}
# --- OVERVIEW mode ---
if ($Mode -eq "overview") {
if ($Mode -eq "overview" -and -not $Section) {
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
@@ -181,6 +293,33 @@ if ($Mode -eq "overview") {
Out "Интерфейс: $cfgIntfCompat"
Out ""
# Panel layout (if file exists)
if ($script:panelLayout) {
$hasPlaced = $false
foreach ($s in @("top","left","right","bottom")) {
if ($script:panelLayout[$s].Count -gt 0) { $hasPlaced = $true; break }
}
if ($hasPlaced) {
Out "--- Раскладка панелей ---"
foreach ($s in @("top","left","right","bottom")) {
if ($script:panelLayout[$s].Count -gt 0) {
Out " $($s.PadRight(7)) $(Format-LayoutSlots $script:panelLayout[$s])"
}
}
Out ""
}
}
# Home page layout (brief summary)
if ($script:homePage) {
$ln = $script:homePage.left.Count
$rn = $script:homePage.right.Count
Out "--- Начальная страница ---"
Out " Шаблон: $($script:homePage.template)"
Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)"
Out ""
}
# Object counts table
Out "--- Состав ($totalObjects объектов) ---"
Out ""
@@ -203,8 +342,34 @@ if ($Mode -eq "overview") {
}
}
# --- Drill-down: -Section home-page ---
if ($Section -eq "home-page") {
if (-not $script:homePage) {
Out "Файл Ext/HomePageWorkArea.xml не найден"
} else {
Out "=== Начальная страница: $cfgName ==="
Out ""
Out "Шаблон: $($script:homePage.template)"
Out ""
foreach ($side in @(@("LeftColumn","left"), @("RightColumn","right"))) {
$items = $script:homePage[$side[1]]
$lbl = $side[0]
if ($items.Count -eq 0) { Out "${lbl}: —"; Out ""; continue }
Out "${lbl} ($($items.Count)):"
foreach ($it in $items) {
Out (Format-HomePageItem $it $true)
foreach ($r in $it.roles) {
$rval = if ($r.value) { "true" } else { "false" }
Out " $($r.name): $rval"
}
}
Out ""
}
}
}
# --- FULL mode ---
if ($Mode -eq "full") {
if ($Mode -eq "full" -and -not $Section) {
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
@@ -275,6 +440,33 @@ if ($Mode -eq "full") {
Out "Обычн.формы в управл.: $useOF"
Out ""
# --- Section: Panel layout ---
if ($script:panelLayout) {
Out "--- Раскладка панелей ---"
foreach ($s in @("top","left","right","bottom")) {
$slots = $script:panelLayout[$s]
if ($slots.Count -gt 0) {
Out " $($s.PadRight(7)) $(Format-LayoutSlots $slots)"
} else {
Out " $($s.PadRight(7))"
}
}
if ($script:panelLayout.declared.Count -gt 0) {
Out " объявлено: $($script:panelLayout.declared -join ', ')"
}
Out ""
}
# --- Section: Home page (brief summary) ---
if ($script:homePage) {
$ln = $script:homePage.left.Count
$rn = $script:homePage.right.Count
Out "--- Начальная страница ---"
Out " Шаблон: $($script:homePage.template)"
Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)"
Out ""
}
# --- Section: Storages & default forms ---
Out "--- Хранилища и формы по умолчанию ---"
$storageProps = @("CommonSettingsStorage","ReportsUserSettingsStorage","ReportsVariantsStorage","FormDataSettingsStorage","DynamicListsUserSettingsStorage","URLExternalDataStorage")
+165 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-info v1.0 — Compact summary of 1C configuration root
# cf-info v1.2 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -13,8 +13,9 @@ sys.stderr.reconfigure(encoding="utf-8")
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
parser.add_argument("-ConfigPath", required=True, help="Path to Configuration.xml or directory")
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
parser.add_argument("-Section", "-Name", choices=["home-page"], default=None, help="Drill-down section (alias: -Name)")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file")
@@ -125,6 +126,108 @@ type_ru_names = {
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
}
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
PANEL_NAMES = {
"cbab57f2-a0f3-4f0a-89ea-4cb19570ab75": "Открытых",
"b553047f-c9aa-4157-978d-448ecad24248": "Разделов",
"13322b22-3960-4d68-93a6-fe2dd7f28ca3": "Избранного",
"c933ac92-92cd-459d-81cc-e0c8a83ced99": "История",
"b2735bd3-d822-4430-ba59-c9e869693b24": "Функций",
}
CAI_NS = "http://v8.1c.ru/8.2/managed-application/core"
def get_panels_layout():
cfg_dir = os.path.dirname(config_path)
cai_path = os.path.join(cfg_dir, "Ext", "ClientApplicationInterface.xml")
if not os.path.isfile(cai_path):
return None
try:
cai_tree = etree.parse(cai_path)
except Exception:
return None
cai_root = cai_tree.getroot()
layout = {"top": [], "left": [], "right": [], "bottom": [], "declared": []}
for side in ("top", "left", "right", "bottom"):
for side_el in cai_root.findall(f"{{{CAI_NS}}}{side}"):
slot = []
for u in side_el.iter(f"{{{CAI_NS}}}uuid"):
key = (u.text or "").strip()
slot.append(PANEL_NAMES.get(key, f"?{key}"))
if slot:
layout[side].append(slot)
for pd in cai_root.findall(f"{{{CAI_NS}}}panelDef"):
key = pd.get("id", "")
layout["declared"].append(PANEL_NAMES.get(key, f"?{key}"))
return layout
def format_layout_slots(slots):
if not slots:
return ""
parts = []
for slot in slots:
if len(slot) == 1:
parts.append(slot[0])
else:
parts.append("Стек(" + ", ".join(slot) + ")")
return " | ".join(parts)
panel_layout = get_panels_layout()
# --- Read home page layout (Ext/HomePageWorkArea.xml) ---
HP_NS = "http://v8.1c.ru/8.3/xcf/extrnprops"
XR_NS_HP = "http://v8.1c.ru/8.3/xcf/readable"
def get_home_page_layout():
cfg_dir = os.path.dirname(config_path)
hp_path = os.path.join(cfg_dir, "Ext", "HomePageWorkArea.xml")
if not os.path.isfile(hp_path):
return None
try:
hp_tree = etree.parse(hp_path)
except Exception:
return None
hp_root = hp_tree.getroot()
result = {"template": "", "left": [], "right": []}
tn = hp_root.find(f"{{{HP_NS}}}WorkingAreaTemplate")
if tn is not None and tn.text:
result["template"] = tn.text.strip()
for col_name, key in (("LeftColumn", "left"), ("RightColumn", "right")):
col = hp_root.find(f"{{{HP_NS}}}{col_name}")
if col is None:
continue
items = []
for it in col.findall(f"{{{HP_NS}}}Item"):
f = it.find(f"{{{HP_NS}}}Form")
h = it.find(f"{{{HP_NS}}}Height")
vis = it.find(f"{{{HP_NS}}}Visibility")
common = True
roles = []
if vis is not None:
cn = vis.find(f"{{{XR_NS_HP}}}Common")
if cn is not None and cn.text:
common = cn.text.strip() == "true"
for v in vis.findall(f"{{{XR_NS_HP}}}Value"):
roles.append({"name": v.get("name", ""), "value": (v.text or "").strip() == "true"})
items.append({
"form": (f.text or "").strip() if f is not None else "",
"height": int((h.text or "10").strip()) if h is not None else 10,
"common": common,
"roles": roles,
})
result[key] = items
return result
home_page = get_home_page_layout()
def format_home_page_item(it, detailed):
badges = [f"h={it['height']}"]
if not it["common"]:
badges.append("скрыта")
if it["roles"]:
badges.append(f"роли: {len(it['roles'])}" if detailed else f"+{len(it['roles'])} ролей")
tail = f" ({', '.join(badges)})" if badges else ""
return f" {it['form']}{tail}"
# --- Count objects in ChildObjects ---
object_counts = OrderedDict()
total_objects = 0
@@ -159,14 +262,14 @@ cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode")
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
# --- BRIEF mode ---
if args.Mode == "brief":
if args.Mode == "brief" and not args.Section:
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
compat_part = f" | {cfg_compat}" if cfg_compat else ""
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
# --- OVERVIEW mode ---
if args.Mode == "overview":
if args.Mode == "overview" and not args.Section:
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
@@ -187,6 +290,20 @@ if args.Mode == "overview":
out(f"Интерфейс: {cfg_intf_compat}")
out()
if panel_layout and any(panel_layout[s] for s in ("top", "left", "right", "bottom")):
out("--- Раскладка панелей ---")
for s in ("top", "left", "right", "bottom"):
if panel_layout[s]:
out(f" {s.ljust(7)} {format_layout_slots(panel_layout[s])}")
out()
# Home page (brief summary)
if home_page:
out("--- Начальная страница ---")
out(f" Шаблон: {home_page['template']}")
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
out()
# Object counts table
out(f"--- Состав ({total_objects} объектов) ---")
out()
@@ -207,7 +324,30 @@ if args.Mode == "overview":
out(f" {padded} {count}")
# --- FULL mode ---
if args.Mode == "full":
# --- Drill-down: -Section home-page ---
if args.Section == "home-page":
if not home_page:
out("Файл Ext/HomePageWorkArea.xml не найден")
else:
out(f"=== Начальная страница: {cfg_name} ===")
out()
out(f"Шаблон: {home_page['template']}")
out()
for col_lbl, col_key in (("LeftColumn", "left"), ("RightColumn", "right")):
items = home_page[col_key]
if not items:
out(f"{col_lbl}: —")
out()
continue
out(f"{col_lbl} ({len(items)}):")
for it in items:
out(format_home_page_item(it, True))
for r in it["roles"]:
rval = "true" if r["value"] else "false"
out(f" {r['name']}: {rval}")
out()
if args.Mode == "full" and not args.Section:
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
@@ -283,6 +423,26 @@ if args.Mode == "full":
out(f"Обычн.формы в управл.: {use_of}")
out()
# --- Section: Panel layout ---
if panel_layout:
out("--- Раскладка панелей ---")
for s in ("top", "left", "right", "bottom"):
slots = panel_layout[s]
if slots:
out(f" {s.ljust(7)} {format_layout_slots(slots)}")
else:
out(f" {s.ljust(7)}")
if panel_layout["declared"]:
out(f" объявлено: {', '.join(panel_layout['declared'])}")
out()
# --- Section: Home page (brief summary) ---
if home_page:
out("--- Начальная страница ---")
out(f" Шаблон: {home_page['template']}")
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
out()
# --- Section: Storages & default forms ---
out("--- Хранилища и формы по умолчанию ---")
storage_props = [
+1 -1
View File
@@ -24,7 +24,7 @@ allowed-tools:
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
```powershell
powershell.exe -NoProfile -File .claude/skills/cf-init/scripts/cf-init.ps1 -Name "МояКонфигурация"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация"
```
## Примеры
+35 -1
View File
@@ -1,4 +1,4 @@
# cf-init v1.1 — Create empty 1C configuration scaffold
# cf-init v1.2 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -192,6 +192,33 @@ $langXml = @"
</MetaDataObject>
"@
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
# via panelDef but not placed by default. Without this file the web client renders
# section icons without labels (icon-only mode).
$openPanelInst = [guid]::NewGuid().ToString()
$sectionsPanelInst = [guid]::NewGuid().ToString()
$caiXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
<top>
<panel id="$openPanelInst">
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
</panel>
</top>
<left>
<panel id="$sectionsPanelInst">
<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
</panel>
</left>
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
</ClientApplicationInterface>
"@
# --- Create directories ---
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
@@ -200,6 +227,10 @@ $langDir = Join-Path $OutputDir "Languages"
if (-not (Test-Path $langDir)) {
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
}
$extDir = Join-Path $OutputDir "Ext"
if (-not (Test-Path $extDir)) {
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
}
# --- Write files with UTF-8 BOM ---
$enc = New-Object System.Text.UTF8Encoding($true)
@@ -207,9 +238,12 @@ $enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
$langFile = Join-Path $langDir "Русский.xml"
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
# --- Output ---
Write-Host "[OK] Создана конфигурация: $Name"
Write-Host " Каталог: $OutputDir"
Write-Host " Configuration.xml: $cfgFile"
Write-Host " Languages: $langFile"
Write-Host " Ext/CAI: $caiFile"
+31 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-init v1.1 — Create empty 1C configuration scaffold
# cf-init v1.2 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration."""
import sys, os, argparse, uuid
@@ -184,20 +184,50 @@ def main():
\t</Language>
</MetaDataObject>'''
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
# via panelDef but not placed by default. Without this file the web client renders
# section icons without labels (icon-only mode).
open_panel_inst = new_uuid()
sections_panel_inst = new_uuid()
cai_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
\t<top>
\t\t<panel id="{open_panel_inst}">
\t\t\t<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
\t\t</panel>
\t</top>
\t<left>
\t\t<panel id="{sections_panel_inst}">
\t\t\t<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
\t\t</panel>
\t</left>
\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
</ClientApplicationInterface>'''
# --- Create directories ---
os.makedirs(output_dir, exist_ok=True)
lang_dir = os.path.join(output_dir, "Languages")
os.makedirs(lang_dir, exist_ok=True)
ext_dir = os.path.join(output_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
# --- Write files ---
write_utf8_bom(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml)
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
write_utf8_bom(cai_file, cai_xml)
print(f"[OK] Создана конфигурация: {name}")
print(f" Каталог: {output_dir}")
print(f" Configuration.xml: {cfg_file}")
print(f" Languages: {lang_file}")
print(f" Ext/CAI: {cai_file}")
if __name__ == '__main__':
main()
+2 -2
View File
@@ -24,6 +24,6 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty"
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty/Configuration.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
```
@@ -1,7 +1,8 @@
# cf-validate v1.1 — Validate 1C configuration root structure
# cf-validate v1.3 — Validate 1C configuration root structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ConfigPath,
[switch]$Detailed,
@@ -145,17 +146,17 @@ $childTypeDirMap = @{
# Valid enum values for Configuration properties
$validEnumValues = @{
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28")
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
"ScriptVariant" = @("Russian","English")
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
"ObjectAutonumerationMode" = @("NotAutoFree","AutoFree")
"ModalityUseMode" = @("DontUse","Use","UseWithWarnings")
"SynchronousPlatformExtensionAndAddInCallUseMode" = @("DontUse","Use","UseWithWarnings")
"InterfaceCompatibilityMode" = @("Taxi","TaxiEnableVersion8_2","Version8_2")
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
"DatabaseTablespacesUseMode" = @("DontUse","Use")
"MainClientApplicationWindowMode" = @("Normal","Fullscreen","Kiosk")
"CompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28")
"CompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
}
# --- 1. Parse XML ---
@@ -203,8 +204,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -ne "2.17" -and $version -ne "2.20") {
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
}
# Must have Configuration child
@@ -535,6 +536,72 @@ if ($childObjNode) {
}
}
# --- Check 9: Form references (HomePageWorkArea + Properties) ---
function Test-FormRef([string]$ref) {
if (-not $ref) { return $true }
# UUID — cannot verify without scanning all forms; skip
if ($ref -match $guidPattern) { return $true }
$parts = $ref.Split(".")
if ($parts.Count -eq 2 -and $parts[0] -eq "CommonForm") {
$p = Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Form.xml"
$pExt = Join-Path (Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Ext") "Form.xml"
return (Test-Path $p) -or (Test-Path $pExt)
}
if ($parts.Count -eq 4 -and $parts[2] -eq "Form" -and $childTypeDirMap.ContainsKey($parts[0])) {
$dir = $childTypeDirMap[$parts[0]]
$p = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Form.xml"
$pExt = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Ext") "Form.xml"
return (Test-Path $p) -or (Test-Path $pExt)
}
return $false
}
$formRefsChecked = 0
$formRefErrors = @()
# HomePageWorkArea
$hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml"
if (Test-Path $hpPath) {
try {
[xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8
$hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable)
$hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops")
foreach ($f in $hpDoc.DocumentElement.SelectNodes("//hp:Item/hp:Form", $hpNs)) {
$ref = $f.InnerText.Trim()
if (-not $ref) { continue }
$formRefsChecked++
if (-not (Test-FormRef $ref)) {
$formRefErrors += "HomePageWorkArea.Form '$ref' — file not found"
}
}
} catch {
$formRefErrors += "HomePageWorkArea.xml: parse error — $($_.Exception.Message)"
}
}
# Properties: DefaultXxxForm refs
if ($propsNode) {
$formProps = @("DefaultReportForm","DefaultReportVariantForm","DefaultReportSettingsForm","DefaultDynamicListSettingsForm","DefaultSearchForm","DefaultDataHistoryChangeHistoryForm","DefaultDataHistoryVersionDataForm","DefaultDataHistoryVersionDifferencesForm","DefaultCollaborationSystemUsersChoiceForm","DefaultConstantsForm")
foreach ($pn in $formProps) {
$node = $propsNode.SelectSingleNode("md:$pn", $ns)
if ($node -and $node.InnerText.Trim()) {
$ref = $node.InnerText.Trim()
$formRefsChecked++
if (-not (Test-FormRef $ref)) {
$formRefErrors += "Properties.$pn '$ref' — form not found"
}
}
}
}
if ($formRefsChecked -eq 0) {
Report-OK "9. Form references: none to check"
} elseif ($formRefErrors.Count -eq 0) {
Report-OK "9. Form references: $formRefsChecked verified"
} else {
foreach ($err in $formRefErrors) { Report-Error "9. $err" }
}
# --- Final output ---
& $finalize
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-validate v1.1 — Validate 1C configuration XML structure
# cf-validate v1.3 — Validate 1C configuration XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
import sys, os, argparse, re
@@ -82,7 +82,7 @@ VALID_ENUM_VALUES = {
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
],
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
'ScriptVariant': ['Russian', 'English'],
@@ -90,7 +90,10 @@ VALID_ENUM_VALUES = {
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
'InterfaceCompatibilityMode': [
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
],
'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
'CompatibilityMode': [
@@ -100,7 +103,7 @@ VALID_ENUM_VALUES = {
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
],
}
@@ -162,7 +165,7 @@ def main():
parser = argparse.ArgumentParser(
description='Validate 1C configuration XML structure', allow_abbrev=False
)
parser.add_argument('-ConfigPath', dest='ConfigPath', required=True)
parser.add_argument('-ConfigPath', '-Path', dest='ConfigPath', required=True)
parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
@@ -228,8 +231,8 @@ def main():
version = root.get('version', '')
if not version:
r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20'):
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
elif version not in ('2.17', '2.20', '2.21'):
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
# Must have Configuration child
cfg_node = None
@@ -534,6 +537,60 @@ def main():
else:
pass # no ChildObjects
# --- Check 9: Form references (HomePageWorkArea + Properties) ---
def test_form_ref(ref):
if not ref:
return True
if GUID_PATTERN.match(ref):
return True
parts = ref.split('.')
if len(parts) == 2 and parts[0] == 'CommonForm':
p = os.path.join(config_dir, 'CommonForms', parts[1], 'Form.xml')
p_ext = os.path.join(config_dir, 'CommonForms', parts[1], 'Ext', 'Form.xml')
return os.path.isfile(p) or os.path.isfile(p_ext)
if len(parts) == 4 and parts[2] == 'Form' and parts[0] in CHILD_TYPE_DIR_MAP:
d = CHILD_TYPE_DIR_MAP[parts[0]]
p = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Form.xml')
p_ext = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Ext', 'Form.xml')
return os.path.isfile(p) or os.path.isfile(p_ext)
return False
form_refs_checked = 0
form_ref_errors = []
hp_path = os.path.join(config_dir, 'Ext', 'HomePageWorkArea.xml')
if os.path.isfile(hp_path):
try:
hp_tree = etree.parse(hp_path)
HP_NS = 'http://v8.1c.ru/8.3/xcf/extrnprops'
for f in hp_tree.getroot().iter(f'{{{HP_NS}}}Form'):
ref = (f.text or '').strip()
if not ref:
continue
form_refs_checked += 1
if not test_form_ref(ref):
form_ref_errors.append(f"HomePageWorkArea.Form '{ref}' — file not found")
except Exception as e:
form_ref_errors.append(f'HomePageWorkArea.xml: parse error — {e}')
if props_node is not None:
form_props = ['DefaultReportForm','DefaultReportVariantForm','DefaultReportSettingsForm','DefaultDynamicListSettingsForm','DefaultSearchForm','DefaultDataHistoryChangeHistoryForm','DefaultDataHistoryVersionDataForm','DefaultDataHistoryVersionDifferencesForm','DefaultCollaborationSystemUsersChoiceForm','DefaultConstantsForm']
for pn in form_props:
node = props_node.find(f'md:{pn}', NS)
if node is not None and node.text and node.text.strip():
ref = node.text.strip()
form_refs_checked += 1
if not test_form_ref(ref):
form_ref_errors.append(f"Properties.{pn} '{ref}' — form not found")
if form_refs_checked == 0:
r.ok('9. Form references: none to check')
elif not form_ref_errors:
r.ok(f'9. Form references: {form_refs_checked} verified')
else:
for err in form_ref_errors:
r.error(f'9. {err}')
# --- Final output ---
r.finalize(out_file)
sys.exit(1 if r.errors > 0 else 0)
+1 -1
View File
@@ -71,7 +71,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
```
## Примеры
+1 -1
View File
@@ -23,7 +23,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-diff/scripts/cfe-diff.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
```
## Mode A — обзор расширения
+1 -1
View File
@@ -44,7 +44,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-init/scripts/cfe-init.ps1 -Name "МоёРасширение"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение"
```
## Примеры
+1 -1
View File
@@ -51,7 +51,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-patch-method/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 -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
```
## Примеры
+2 -2
View File
@@ -24,6 +24,6 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src"
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src/Configuration.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
```
@@ -1,7 +1,8 @@
# cfe-validate v1.3 — Validate 1C configuration extension structure (CFE)
# cfe-validate v1.4 — Validate 1C configuration extension structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ExtensionPath,
[switch]$Detailed,
@@ -145,10 +146,10 @@ $childTypeDirMap = @{
# Valid enum values for extension properties
$validEnumValues = @{
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28")
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
"ScriptVariant" = @("Russian","English")
"InterfaceCompatibilityMode" = @("Taxi","TaxiEnableVersion8_2","Version8_2")
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
}
# --- 1. Parse XML ---
@@ -196,8 +197,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -ne "2.17" -and $version -ne "2.20") {
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
}
# Must have Configuration child
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-validate v1.3 — Validate 1C configuration extension XML structure (CFE)
# cfe-validate v1.4 — Validate 1C configuration extension XML structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
import sys, os, argparse, re
@@ -82,11 +82,14 @@ VALID_ENUM_VALUES = {
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
],
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
'ScriptVariant': ['Russian', 'English'],
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
'InterfaceCompatibilityMode': [
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
],
}
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
@@ -147,7 +150,7 @@ def main():
parser = argparse.ArgumentParser(
description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False
)
parser.add_argument('-ExtensionPath', dest='ExtensionPath', required=True)
parser.add_argument('-ExtensionPath', '-Path', dest='ExtensionPath', required=True)
parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
@@ -213,8 +216,8 @@ def main():
version = root.get('version', '')
if not version:
r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20'):
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
elif version not in ('2.17', '2.20', '2.21'):
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
# Must have Configuration child
cfg_node = None
+6 -6
View File
@@ -1,6 +1,6 @@
---
name: db-create
description: Создание информационной базы 1С. Используй когда пользователь просит создать базу, новую ИБ, пустую базу
description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу
argument-hint: <path|name>
allowed-tools:
- Bash
@@ -31,7 +31,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры>
```
### Параметры скрипта
@@ -65,14 +65,14 @@ powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 <
```powershell
# Создать файловую базу
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB"
# Создать серверную базу
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
# Создать из шаблона CF
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
# Создать и добавить в список баз
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
```
+5 -5
View File
@@ -1,6 +1,6 @@
---
name: db-dump-cf
description: Выгрузка конфигурации 1С в CF-файл. Используй когда пользователь просит выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
argument-hint: "[database] [output.cf]"
allowed-tools:
- Bash
@@ -35,7 +35,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры>
```
### Параметры скрипта
@@ -69,11 +69,11 @@ powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1
```powershell
# Выгрузка конфигурации (файловая база)
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
# Выгрузка расширения
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
```
+7 -7
View File
@@ -1,6 +1,6 @@
---
name: db-dump-xml
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда пользователь просит выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
argument-hint: "[database] [outputDir]"
allowed-tools:
- Bash
@@ -37,7 +37,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры>
```
### Параметры скрипта
@@ -81,17 +81,17 @@ powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.p
```powershell
# Полная выгрузка (файловая база)
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Инкрементальная выгрузка
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
# Частичная выгрузка
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Выгрузка расширения
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
```
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: db-list
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда пользователь говорит про базы данных, список баз, "добавь базу", "какие базы есть"
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз список баз, зарегистрировать базу в реестре, какие базы есть
argument-hint: "[add|remove|show]"
allowed-tools:
- Read
+5 -5
View File
@@ -1,6 +1,6 @@
---
name: db-load-cf
description: Загрузка конфигурации 1С из CF-файла. Используй когда пользователь просит загрузить конфигурацию из CF, восстановить из бэкапа CF
description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF
argument-hint: <input.cf> [database]
allowed-tools:
- Bash
@@ -36,7 +36,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры>
```
### Параметры скрипта
@@ -71,11 +71,11 @@ powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1
```powershell
# Файловая база
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
# Загрузка расширения
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
```
+78 -78
View File
@@ -1,78 +1,78 @@
---
name: db-load-git
description: Загрузка изменений из Git в базу 1С. Используй когда пользователь просит загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
argument-hint: "[database] [source]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-load-git — Загрузка изменений из Git
Определяет изменённые файлы конфигурации по данным Git и выполняет частичную загрузку в информационную базу.
## Usage
```
/db-load-git [database]
/db-load-git dev — все незафиксированные изменения
/db-load-git dev -Source Staged — только staged
/db-load-git dev -Source Commit -CommitRange "HEAD~3..HEAD"
/db-load-git dev -DryRun — только показать что будет загружено
```
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.ps1 <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог XML-выгрузки (git-репозиторий) |
| `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` |
| `-CommitRange <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) |
| `-Extension <имя>` | нет | Загрузить в расширение |
| `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## После выполнения
1. Показать список загруженных файлов и результат из лога
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
## Примеры
```powershell
# Все незафиксированные изменения
powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.ps1 -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
# Из диапазона коммитов
powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
```
---
name: db-load-git
description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
argument-hint: "[database] [source]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-load-git — Загрузка изменений из Git
Определяет изменённые файлы конфигурации по данным Git и выполняет частичную загрузку в информационную базу.
## Usage
```
/db-load-git [database]
/db-load-git dev — все незафиксированные изменения
/db-load-git dev -Source Staged — только staged
/db-load-git dev -Source Commit -CommitRange "HEAD~3..HEAD"
/db-load-git dev -DryRun — только показать что будет загружено
```
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог XML-выгрузки (git-репозиторий) |
| `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` |
| `-CommitRange <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) |
| `-Extension <имя>` | нет | Загрузить в расширение |
| `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## После выполнения
1. Показать список загруженных файлов и результат из лога
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
## Примеры
```powershell
# Все незафиксированные изменения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
# Из диапазона коммитов
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
```
+6 -6
View File
@@ -1,6 +1,6 @@
---
name: db-load-xml
description: Загрузка конфигурации 1С из XML-файлов. Используй когда пользователь просит загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
argument-hint: <configDir> [database]
allowed-tools:
- Bash
@@ -38,7 +38,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры>
```
### Параметры скрипта
@@ -96,14 +96,14 @@ Documents/Заказ/Forms/ФормаДокумента.xml
```powershell
# Полная загрузка
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Частичная загрузка конкретных файлов
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
# Загрузка расширения
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
# Загрузка + обновление БД в одном запуске
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
```
+6 -6
View File
@@ -1,6 +1,6 @@
---
name: db-run
description: Запуск 1С:Предприятие. Используй когда пользователь просит запустить 1С, открыть базу, запустить предприятие
description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие
argument-hint: "[database]"
allowed-tools:
- Bash
@@ -36,7 +36,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры>
```
### Параметры скрипта
@@ -63,14 +63,14 @@ powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 <пар
```powershell
# Простой запуск
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Запуск с обработкой
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
# Открыть по навигационной ссылке
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
# Серверная база с параметром запуска
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
```
+5 -5
View File
@@ -1,6 +1,6 @@
---
name: db-update
description: Обновление конфигурации базы данных 1С. Используй когда пользователь просит обновить БД, применить конфигурацию, UpdateDBCfg
description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg
argument-hint: "[database]"
allowed-tools:
- Bash
@@ -35,7 +35,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры>
```
### Параметры скрипта
@@ -83,11 +83,11 @@ powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 <
```powershell
# Обычное обновление (файловая база)
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Динамическое обновление (серверная база)
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
# Обновление расширения
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
```
-60
View File
@@ -1,60 +0,0 @@
---
name: epf-add-form
description: Добавить управляемую форму к внешней обработке 1С
argument-hint: <ProcessorName> <FormName> [Synonym]
allowed-tools:
- Bash
- Read
- Write
- Edit
- Glob
- Grep
---
# /epf-add-form — Добавление формы
Создаёт управляемую форму и регистрирует её в корневом XML обработки.
## Usage
```
/epf-add-form <ProcessorName> <FormName> [Synonym] [--main]
```
| Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|-------------------------------------------|
| ProcessorName | да | — | Имя обработки (должна существовать) |
| FormName | да | — | Имя формы |
| Synonym | нет | = FormName | Синоним формы |
| --main | нет | авто | Установить как форму по умолчанию (автоматически для первой формы) |
| SrcDir | нет | `src` | Каталог исходников |
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-add-form/scripts/add-form.ps1 -ProcessorName "<ProcessorName>" -FormName "<FormName>" [-Synonym "<Synonym>"] [-Main] [-SrcDir "<SrcDir>"]
```
## Что создаётся
```
<SrcDir>/<ProcessorName>/Forms/
├── <FormName>.xml # Метаданные формы (1 UUID)
└── <FormName>/
└── Ext/
├── Form.xml # Описание формы (logform namespace)
└── Form/
└── Module.bsl # BSL-модуль с 4 регионами
```
## Что модифицируется
- `<SrcDir>/<ProcessorName>.xml` — добавляется `<Form>` в `ChildObjects`, обновляется `DefaultForm` (автоматически если это первая форма, или явно при `--main`)
## Детали
- FormType: Managed
- UsePurposes: PlatformApplication, MobilePlatformApplication
- AutoCommandBar с id=-1
- Реквизит "Объект" с MainAttribute=true
- BSL-модуль содержит 5 регионов: ОбработчикиСобытийФормы, ОбработчикиСобытийЭлементовФормы, ОбработчикиКомандФормы, ОбработчикиОповещений, СлужебныеПроцедурыИФункции
@@ -1,226 +0,0 @@
# epf-add-form v1.1 — Add managed form to 1C processor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$ProcessorName,
[Parameter(Mandatory)]
[string]$FormName,
[string]$Synonym = $FormName,
[switch]$Main,
[string]$SrcDir = "src"
)
$ErrorActionPreference = "Stop"
# --- Detect format version ---
function Detect-FormatVersion([string]$dir) {
$d = $dir
while ($d) {
$cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) {
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
if ($parent -eq $d) { break }
$d = $parent
}
return "2.17"
}
$formatVersion = Detect-FormatVersion (Resolve-Path $SrcDir).Path
# --- Проверки ---
$rootXmlPath = Join-Path $SrcDir "$ProcessorName.xml"
if (-not (Test-Path $rootXmlPath)) {
Write-Error "Корневой файл обработки не найден: $rootXmlPath. Сначала выполните epf-init."
exit 1
}
$processorDir = Join-Path $SrcDir $ProcessorName
$formsDir = Join-Path $processorDir "Forms"
$formMetaPath = Join-Path $formsDir "$FormName.xml"
if (Test-Path $formMetaPath) {
Write-Error "Форма уже существует: $formMetaPath"
exit 1
}
# --- Создание каталогов ---
$formDir = Join-Path $formsDir $FormName
$formExtDir = Join-Path $formDir "Ext"
$formModuleDir = Join-Path $formExtDir "Form"
New-Item -ItemType Directory -Path $formModuleDir -Force | Out-Null
# --- Кодировка ---
$encBom = New-Object System.Text.UTF8Encoding($true)
$encNoBom = New-Object System.Text.UTF8Encoding($false)
# --- 1. Метаданные формы (Forms/<FormName>.xml) ---
$formUuid = [guid]::NewGuid().ToString()
$formMetaXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
<Form uuid="$formUuid">
<Properties>
<Name>$FormName</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content>
</v8:item>
</Synonym>
<Comment/>
<FormType>Managed</FormType>
<IncludeHelpInContents>false</IncludeHelpInContents>
<UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
</UsePurposes>
<ExtendedPresentation/>
</Properties>
</Form>
</MetaDataObject>
"@
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
# --- 2. Описание формы (Forms/<FormName>/Ext/Form.xml) ---
$formXmlPath = Join-Path $formExtDir "Form.xml"
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<ChildItems/>
<Attributes>
<Attribute name="Объект" id="1">
<Type>
<v8:Type>cfg:ExternalDataProcessorObject.$ProcessorName</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>
</Attribute>
</Attributes>
</Form>
"@
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
# --- 3. BSL-модуль (Forms/<FormName>/Ext/Form/Module.bsl) ---
$modulePath = Join-Path $formModuleDir "Module.bsl"
$moduleBsl = @"
#Область ОбработчикиСобытийФормы
#КонецОбласти
#Область ОбработчикиСобытийЭлементовФормы
#КонецОбласти
#Область ОбработчикиКомандФормы
#КонецОбласти
#Область ОбработчикиОповещений
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
"@
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
# --- 4. Модификация корневого XML ---
$rootXmlFull = Resolve-Path $rootXmlPath
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load($rootXmlFull.Path)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$childObjects = $xmlDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
if (-not $childObjects) {
Write-Error "Не найден элемент ChildObjects в $rootXmlPath"
exit 1
}
# Добавить <Form> перед первым <Template>, или в конец
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formElem.InnerText = $FormName
$firstTemplate = $childObjects.SelectSingleNode("md:Template", $nsMgr)
if ($firstTemplate) {
# Вставить перед Template, добавив перенос строки + табуляцию
$whitespace = $xmlDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($whitespace, $firstTemplate) | Out-Null
$childObjects.InsertBefore($formElem, $whitespace) | Out-Null
} else {
# Добавить в конец ChildObjects
# Если ChildObjects пустой (самозакрывающийся), нужно добавить форматирование
if ($childObjects.ChildNodes.Count -eq 0) {
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($formElem) | Out-Null
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
} else {
$lastChild = $childObjects.LastChild
# Вставить перед закрывающим whitespace (если есть), или в конец
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$childObjects.InsertBefore($xmlDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
$childObjects.InsertBefore($formElem, $lastChild) | Out-Null
} else {
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($formElem) | Out-Null
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
}
}
}
# Обновить DefaultForm: явно при -Main, или автоматически если это первая форма
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
$isFirstForm = ($existingForms.Count -eq 1)
if ($Main -or $isFirstForm) {
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr)
if ($defaultForm) {
$defaultForm.InnerText = "ExternalDataProcessor.$ProcessorName.Form.$FormName"
}
}
# Сохранить с BOM
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom
$settings.Indent = $false # Preserve original whitespace
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
$xmlDoc.Save($writer)
$writer.Close()
$stream.Close()
Write-Host "[OK] Создана форма: $FormName"
Write-Host " Метаданные: $formMetaPath"
Write-Host " Описание: $formXmlPath"
Write-Host " Модуль: $modulePath"
if ($Main -or $isFirstForm) {
Write-Host " DefaultForm обновлён"
}
@@ -1,272 +0,0 @@
#!/usr/bin/env python3
# add-form v1.1 — Add managed form to 1C external data processor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
import uuid
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def detect_format_version(d):
while d:
cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f:
head = f.read(2000)
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
if m:
return m.group(1)
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return "2.17"
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
xml_bytes += b"\n"
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Add managed form to 1C processor", allow_abbrev=False)
parser.add_argument("-ProcessorName", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-Synonym", default=None)
parser.add_argument("-Main", action="store_true")
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
processor_name = args.ProcessorName
form_name = args.FormName
synonym = args.Synonym if args.Synonym is not None else form_name
is_main = args.Main
src_dir = args.SrcDir
format_version = detect_format_version(os.path.abspath(src_dir))
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{processor_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}. Сначала выполните epf-init.", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, processor_name)
forms_dir = os.path.join(processor_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
if os.path.exists(form_meta_path):
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Create directories ---
form_dir = os.path.join(forms_dir, form_name)
form_ext_dir = os.path.join(form_dir, "Ext")
form_module_dir = os.path.join(form_ext_dir, "Form")
os.makedirs(form_module_dir, exist_ok=True)
# --- 1. Form metadata (Forms/<FormName>.xml) ---
form_uuid = str(uuid.uuid4())
form_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
f' version="{format_version}">\n'
f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{form_name}</Name>\n'
'\t\t\t<Synonym>\n'
'\t\t\t\t<v8:item>\n'
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
'\t\t\t\t</v8:item>\n'
'\t\t\t</Synonym>\n'
'\t\t\t<Comment/>\n'
'\t\t\t<FormType>Managed</FormType>\n'
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
'\t\t\t<UsePurposes>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n'
'\t\t\t<ExtendedPresentation/>\n'
'\t\t</Properties>\n'
'\t</Form>\n'
'</MetaDataObject>'
)
write_text_with_bom(form_meta_path, form_meta_xml)
# --- 2. Form description (Forms/<FormName>/Ext/Form.xml) ---
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
form_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
f' version="{format_version}">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:ExternalDataProcessorObject.{processor_name}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
write_text_with_bom(form_xml_path, form_xml)
# --- 3. BSL module (Forms/<FormName>/Ext/Form/Module.bsl) ---
module_path = os.path.join(form_module_dir, "Module.bsl")
module_bsl = (
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
)
write_text_with_bom(module_path, module_bsl)
# --- 4. Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
ns = "http://v8.1c.ru/8.3/MDClasses"
child_objects = root.find(".//md:ChildObjects", NSMAP)
if child_objects is None:
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
sys.exit(1)
# Add <Form> before first <Template>, or at end
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
first_template = child_objects.find("md:Template", NSMAP)
if first_template is not None:
# Insert before Template, adding newline + indent
idx = list(child_objects).index(first_template)
child_objects.insert(idx, form_elem)
# Set whitespace: form_elem gets same tail pattern
form_elem.tail = "\n\t\t\t"
else:
# Add to end of ChildObjects
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
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"
# Update DefaultForm: explicitly with -Main, or automatically if this is the first form
existing_forms = child_objects.findall("md:Form", NSMAP)
is_first_form = len(existing_forms) == 1
if is_main or is_first_form:
default_form = root.find(".//md:DefaultForm", NSMAP)
if default_form is not None:
default_form.text = f"ExternalDataProcessor.{processor_name}.Form.{form_name}"
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Создана форма: {form_name}")
print(f" Метаданные: {form_meta_path}")
print(f" Описание: {form_xml_path}")
print(f" Модуль: {module_path}")
if is_main or is_first_form:
print(" DefaultForm обновлён")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: epf-bsp-add-command
description: Добавить команду в дополнительную обработку БСП
description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
allowed-tools:
- Read
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: epf-bsp-init
description: Добавить функцию регистрации БСП (СведенияОВнешнейОбработке) в модуль объекта обработки
description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки»
argument-hint: <ProcessorName> <Вид>
allowed-tools:
- Read
@@ -203,6 +203,6 @@ allowed-tools:
## Дальнейшие шаги
- Добавить ещё команду: `/epf-bsp-add-command`
- Добавить форму: `/epf-add-form`
- Добавить форму: `/form-add`
- Добавить макет: `/template-add`
- Собрать EPF: `/epf-build`
+3 -3
View File
@@ -40,7 +40,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры>
```
### Параметры скрипта
@@ -62,8 +62,8 @@ powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <
```powershell
# Сборка обработки (файловая база)
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
```
+3 -3
View File
@@ -39,7 +39,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры>
```
### Параметры скрипта
@@ -62,8 +62,8 @@ powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <п
```powershell
# Разборка обработки (файловая база)
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
```
+3 -3
View File
@@ -1,6 +1,6 @@
---
name: epf-init
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников)
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля
argument-hint: <Name> [Synonym]
allowed-tools:
- Bash
@@ -30,12 +30,12 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
```
## Дальнейшие шаги
- Добавить форму: `/epf-add-form`
- Добавить форму: `/form-add`
- Добавить макет: `/template-add`
- Добавить справку: `/help-add`
- Собрать EPF: `/epf-build`
+3 -1
View File
@@ -1,4 +1,4 @@
# epf-init v1.0 — Init 1C external data processor scaffold
# epf-init v1.1 — Init 1C external data processor scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -10,6 +10,8 @@ param(
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString()
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# epf-init v1.0 — Init 1C external data processor scaffold
# epf-init v1.1 — Init 1C external data processor scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external data processor."""
import sys, os, argparse, uuid
+2 -2
View File
@@ -24,7 +24,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка"
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка/МояОбработка.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml"
```
@@ -1,8 +1,9 @@
# epf-validate v1.1 — Validate 1C external data processor / report structure
# epf-validate v1.2 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ObjectPath,
[switch]$Detailed,
@@ -184,8 +185,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -ne "2.17" -and $version -ne "2.20") {
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
}
# Detect type: ExternalDataProcessor or ExternalReport
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# epf-validate v1.1 — Validate 1C external data processor / report structure
# epf-validate v1.2 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
@@ -46,7 +46,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Validate 1C external data processor/report structure", allow_abbrev=False)
parser.add_argument("-ObjectPath", required=True)
parser.add_argument("-ObjectPath", "-Path", required=True)
parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30)
parser.add_argument("-OutFile", default=None)
@@ -165,8 +165,8 @@ def main():
version = root.get("version", "")
if not version:
report_warn("1. Missing version attribute on MetaDataObject")
elif version not in ("2.17", "2.20"):
report_warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
elif version not in ("2.17", "2.20", "2.21"):
report_warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
# Detect type
child_elements = []
+3 -3
View File
@@ -42,7 +42,7 @@ allowed-tools:
Используй общий скрипт из epf-build:
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры>
```
### Параметры скрипта
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <
```powershell
# Сборка отчёта (файловая база)
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
```
+3 -3
View File
@@ -41,7 +41,7 @@ allowed-tools:
Используй общий скрипт из epf-dump:
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры>
```
### Параметры скрипта
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <п
```powershell
# Разборка отчёта (файловая база)
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
```
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: erf-init
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников)
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля
argument-hint: <Name> [Synonym] [--with-skd]
allowed-tools:
- Bash
@@ -31,7 +31,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/erf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
```
## Дальнейшие шаги
+3 -1
View File
@@ -1,4 +1,4 @@
# erf-init v1.0 — Init 1C external report scaffold
# erf-init v1.1 — Init 1C external report scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -12,6 +12,8 @@ param(
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString()
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# erf-init v1.0 — Init 1C external report scaffold
# erf-init v1.1 — Init 1C external report scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external report."""
import sys, os, argparse, uuid
+2 -2
View File
@@ -26,7 +26,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт"
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
```
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: form-add
description: Добавить управляемую форму к объекту конфигурации 1С
description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
allowed-tools:
- Bash
@@ -32,7 +32,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/form-add/scripts/form-add.ps1 -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
```
## Purpose — назначение формы
+10 -17
View File
@@ -1,4 +1,4 @@
# form-add v1.3 — Add managed form to 1C config object
# form-add v1.5 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -15,6 +15,8 @@ param(
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Detect XML format version ---
@@ -219,9 +221,6 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<Events>
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
</Events>
<ChildItems/>
<Attributes>
<Attribute name="Список" id="1">
@@ -247,9 +246,6 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<Events>
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
</Events>
<ChildItems/>
<Attributes>
<Attribute name="$mainAttrName" id="1">
@@ -285,23 +281,25 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
$savedDataLine = ""
if ($objectType -notin $processorLikeTypes) {
$savedDataLine = "`n`t`t`t<SavedData>true</SavedData>"
}
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<Events>
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
</Events>
<ChildItems/>
<Attributes>
<Attribute name="$mainAttrName" id="1">
<Type>
<v8:Type>cfg:$mainAttrType</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>
<SavedData>true</SavedData>
<MainAttribute>true</MainAttribute>$savedDataLine
</Attribute>
</Attributes>
</Form>
@@ -321,11 +319,6 @@ $modulePath = Join-Path $formModuleDir "Module.bsl"
$moduleBsl = @"
#Область ОбработчикиСобытийФормы
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
КонецПроцедуры
#КонецОбласти
#Область ОбработчикиСобытийЭлементовФормы
+7 -16
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-add v1.3 — Add managed form to 1C config object
# form-add v1.5 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -248,9 +248,6 @@ def main():
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
@@ -277,9 +274,6 @@ def main():
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
@@ -315,15 +309,17 @@ def main():
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
saved_data_line = ''
if object_type not in processor_like_types:
saved_data_line = '\t\t\t<SavedData>true</SavedData>\n'
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="{format_version}">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
@@ -331,7 +327,7 @@ def main():
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<SavedData>true</SavedData>\n'
f'{saved_data_line}'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
@@ -349,11 +345,6 @@ def main():
module_bsl = (
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\n'
'\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 \u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435(\u041e\u0442\u043a\u0430\u0437, \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430)\n'
'\n'
'\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
+95 -8
View File
@@ -29,10 +29,10 @@ allowed-tools:
```powershell
# Режим JSON DSL
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -JsonPath "<json>" -OutputPath "<Form.xml>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -JsonPath "<json>" -OutputPath "<Form.xml>"
# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog)
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
```
## JSON DSL — справка
@@ -62,8 +62,10 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| DSL ключ | XML элемент | Значение ключа |
|--------------|-------------------|---------------------------------------------------|
| `"group"` | UsualGroup | `"horizontal"` / `"vertical"` / `"alwaysHorizontal"` / `"alwaysVertical"` / `"collapsible"` |
| `"columnGroup"` | ColumnGroup | `"horizontal"` / `"vertical"` / `"inCell"` — только внутри `columns` таблицы |
| `"input"` | InputField | имя элемента |
| `"check"` | CheckBoxField | имя |
| `"radio"` | RadioButtonField | имя |
| `"label"` | LabelDecoration | имя (текст задаётся через `title`) |
| `"labelField"` | LabelField | имя |
| `"table"` | Table | имя |
@@ -74,6 +76,7 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `"picField"` | PictureField | имя |
| `"calendar"` | CalendarField | имя |
| `"cmdBar"` | CommandBar | имя |
| `"autoCmdBar"` | AutoCommandBar формы | имя — наполняет главную АКП формы (id=-1), не попадает в `<ChildItems>` |
| `"popup"` | Popup | имя |
### Общие свойства (все типы элементов)
@@ -96,7 +99,7 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
**input / picField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `AutoComplete`, `TextEditEnd`, `Clearing`, `Creating`, `EditTextChange`
**check**: `OnChange`
**check / radio**: `OnChange`
**table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `ValueChoice`, `BeforeAddRow`, `BeforeDeleteRow`, `AfterDeleteRow`, `BeforeRowChange`, `BeforeEditEnd`, `OnActivateRow`, `OnActivateCell`, `Drag`, `DragStart`, `DragCheck`, `DragEnd`
@@ -124,7 +127,8 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `skipOnInput: true` | Пропускать при обходе Tab | |
| `inputHint` | Подсказка в пустом поле | `"Введите наименование..."` |
| `width` / `height` | Размер | числа |
| `autoMaxWidth: false` | Отключить авто-ширину | для фиксированных полей |
| `autoMaxWidth: false` | Снять авто-ограничение ширины (поле растянется) | |
| `maxWidth` / `maxHeight` | Жёсткое ограничение размера | числа; обычно вместе с `autoMaxWidth: false` |
| `horizontalStretch: true` | Растягивать по ширине | |
### Чекбокс (check)
@@ -134,6 +138,37 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `path` | DataPath |
| `titleLocation` | Размещение заголовка |
### Поле переключателя (radio)
Радиокнопки или тумблер для выбора одного значения из списка.
| Ключ | Описание | Пример |
|------|----------|--------|
| `path` | DataPath — привязка к реквизиту | `"СпособКурса"` |
| `radioButtonType` | Вид переключателя | `"Auto"` (по умолчанию), `"RadioButtons"`, `"Tumbler"` |
| `columnsCount` | Число колонок раскладки | `1`, `2`, ... |
| `titleLocation` | Размещение заголовка | по умолчанию `"none"` |
| `choiceList` | Список вариантов: массив `{value, presentation}` | см. ниже |
`choiceList[*]`:
| Ключ | Описание |
|------|----------|
| `value` | Значение варианта. Строка/число/булево; для перечисления — `"Enum.ИмяТипа.EnumValue.ИмяЗначения"` |
| `presentation` | Текст рядом с переключателем. Строка (русский) либо объект `{ru, en, ...}` для мультиязычности |
```json
{
"radio": "СпособКурса",
"path": "Объект.СпособУстановкиКурса",
"radioButtonType": "Auto",
"choiceList": [
{ "value": "Enum.СпособыКурса.EnumValue.Авто", "presentation": { "ru": "Автоматически", "en": "Automatic" } },
{ "value": "Enum.СпособыКурса.EnumValue.Ручной", "presentation": "вручную" }
]
}
```
### Надпись-декорация (label)
| Ключ | Описание |
@@ -149,7 +184,8 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| Ключ | Описание |
|------|----------|
| `showTitle: true` | Показывать заголовок группы |
| `united: false` | Не объединять рамку |
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
| `collapsed: true` | Только для `"group": "collapsible"` — группа создаётся свёрнутой |
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
| `children: [...]` | Вложенные элементы |
@@ -175,6 +211,35 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `rowPictureDataPath` | Путь к картинке строки (напр. `"Список.DefaultPicture"`) |
| `tableAutofill: false` | Управление Autofill внутреннего AutoCommandBar |
Колонки можно группировать через `columnGroup` (см. ниже).
### Группа колонок (columnGroup)
Используется только внутри `columns` таблицы. Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"inCell"` (склеивает колонки в одну ячейку шапки). Допускается вложение `columnGroup` в `columnGroup`.
| Ключ | Описание |
|------|----------|
| `name` | Имя элемента (рекомендуется задавать явно) |
| `title` | Заголовок группы |
| `showTitle: false` | Скрыть заголовок |
| `showInHeader: true/false` | Показывать ли группу в шапке таблицы |
| `width` | Ширина |
| `horizontalStretch: false` | Растягивание |
| `children: [...]` | Колонки внутри группы (`input`, `labelField`, `picField`, вложенный `columnGroup` …) |
```json
{ "table": "Список", "path": "Список", "columns": [
{ "columnGroup": "horizontal", "name": "ГруппаДата", "title": "Срок", "children": [
{ "input": "СрокИсполнения", "path": "Список.СрокИсполнения" },
{ "labelField": "Просрочено", "path": "Список.Просрочено" }
]},
{ "columnGroup": "inCell", "name": "ГруппаИсполнитель", "showInHeader": true, "children": [
{ "input": "Исполнитель", "path": "Список.Исполнитель" }
]},
{ "input": "Комментарий", "path": "Список.Комментарий" }
]}
```
### Страницы (pages + page)
| Ключ (pages) | Описание |
@@ -195,18 +260,39 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `command` | Имя команды формы → `Form.Command.Имя` |
| `stdCommand` | Стандартная команда: `"Close"``Form.StandardCommand.Close`; с точкой: `"Товары.Add"``Form.Item.Товары.StandardCommand.Add` |
| `defaultButton: true` | Кнопка по умолчанию |
| `type` | `"usual"`, `"hyperlink"`, `"commandBar"` |
| `type` | `"usual"`, `"hyperlink"`. По умолчанию `"usual"`. Конкретный XML-вид (UsualButton/Hyperlink/CommandBarButton/CommandBarHyperlink) подставляется автоматически по контексту |
| `picture` | Картинка кнопки |
| `representation` | `"Auto"`, `"Text"`, `"Picture"`, `"PictureAndText"` |
| `locationInCommandBar` | `"Auto"`, `"InCommandBar"`, `"InAdditionalSubmenu"` |
### Командная панель (cmdBar)
Дополнительная пользовательская панель команд, размещается как обычный элемент в layout формы.
| Ключ | Описание |
|------|----------|
| `autofill: true` | Автозаполнение стандартными командами |
| `children: [...]` | Кнопки панели |
### Главная автокомандная панель формы (autoCmdBar)
Наполняет встроенную AutoCommandBar формы (id=-1) кастомными кнопками. Указывать только если нужно добавить свои кнопки на главную панель или явно управлять автозаполнением.
| Ключ | Описание |
|------|----------|
| `autofill: true/false` | Автозаполнение стандартными командами |
| `horizontalAlign` | `"Left"` / `"Center"` / `"Right"` |
| `children: [...]` | Кнопки/popup |
```json
{ "autoCmdBar": "ФормаКоманднаяПанель", "autofill": true, "children": [
{ "button": "ИзменитьВыделенные", "command": "ИзменитьВыделенные",
"locationInCommandBar": "InAdditionalSubmenu" }
]}
```
Кнопки основных действий формы и подменю размещают здесь, а не в отдельной группе на форме. Отдельной кнопкой в layout — только если она логически привязана к конкретному полю или группе.
### Выпадающее меню (popup)
| Ключ | Описание |
@@ -239,6 +325,7 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
```
- `savedData: true` — сохраняемые данные
- `main: true` — главный реквизит формы (например, основной `*Object.*`, `DynamicList`, `*RecordSet.*`)
### Команды (commands)
@@ -348,9 +435,9 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
{ "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" }
]},
{ "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" },
{ "group": "horizontal", "name": "ГруппаКнопок", "children": [
{ "autoCmdBar": "ФормаКоманднаяПанель", "children": [
{ "button": "Загрузить", "command": "Загрузить", "defaultButton": true },
{ "button": "Закрыть", "stdCommand": "Close" }
{ "button": "Закрыть", "stdCommand": "Close" }
]}
],
"attributes": [
@@ -1,4 +1,4 @@
# form-compile v1.6 — Compile 1C managed form from JSON or object metadata
# form-compile v1.21 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$JsonPath,
@@ -668,6 +668,7 @@ function Generate-DocumentListDSL($meta, [hashtable]$p) {
$tableEl = [ordered]@{
table = "Список"; path = "Список"
rowPictureDataPath = "Список.DefaultPicture"
commandBarLocation = "None"
tableAutofill = $false
columns = $columns
@@ -1003,6 +1004,7 @@ function Generate-InformationRegisterListDSL($meta, [hashtable]$p) {
$tableEl = [ordered]@{
table = "Список"; path = "Список"
rowPictureDataPath = "Список.DefaultPicture"
commandBarLocation = "None"
tableAutofill = $false
columns = $columns
@@ -1060,6 +1062,7 @@ function Generate-AccumulationRegisterListDSL($meta, [hashtable]$p) {
$tableEl = [ordered]@{
table = "Список"; path = "Список"
rowPictureDataPath = "Список.DefaultPicture"
commandBarLocation = "None"
tableAutofill = $false
columns = $columns
@@ -1554,7 +1557,7 @@ $script:formTypeSynonyms["определяемыйтип"] = "Define
# Known invalid types (runtime/UI types that don't exist in XDTO schema)
$script:knownInvalidTypes = @{
"FormDataStructure" = "Runtime type. Use cfg:*Object.XXX (e.g. CatalogObject.XXX)"
"FormDataStructure" = "Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)"
"FormDataCollection" = "Runtime type. Use ValueTable"
"FormDataTree" = "Runtime type. Use ValueTree"
"FormDataTreeItem" = "Runtime type, not valid in XML"
@@ -1569,6 +1572,8 @@ $script:knownInvalidTypes = @{
function Resolve-TypeStr {
param([string]$typeStr)
if (-not $typeStr) { return $typeStr }
# Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix)
if ($typeStr -match '^cfg:(.+)$') { $typeStr = $Matches[1] }
if ($typeStr -match '^([^(]+)\((.+)\)$') {
$base = $Matches[1].Trim(); $params = $Matches[2]
$r = $script:formTypeSynonyms[$base.ToLower()]
@@ -1769,6 +1774,7 @@ function Get-ElementName {
$script:knownEvents = @{
"input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange")
"check" = @("OnChange")
"radio" = @("OnChange")
"label" = @("Click","URLProcessing")
"labelField"= @("OnChange","StartChoice","ChoiceProcessing","Click","URLProcessing","Clearing")
"table" = @("Selection","BeforeAddRow","AfterDeleteRow","BeforeDeleteRow","OnActivateRow","OnEditEnd","OnStartEdit","BeforeRowChange","BeforeEditEnd","ValueChoice","OnActivateCell","OnActivateField","Drag","DragStart","DragCheck","DragEnd","OnGetDataAtServer","BeforeLoadUserSettingsAtServer","OnUpdateUserSettingSetAtServer","OnChange")
@@ -1819,13 +1825,60 @@ function Emit-Companion {
}
function Emit-Element {
param($el, [string]$indent)
param($el, [string]$indent, [bool]$inCmdBar = $false)
# Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio).
# Maps any synonym to canonical short DSL key.
$synonyms = @{
"commandBar" = "cmdBar"
"autoCommandBar" = "autoCmdBar"
"КоманднаяПанель" = "cmdBar"
"InputField" = "input"
"ПолеВвода" = "input"
"CheckBoxField" = "check"
"ПолеФлажка" = "check"
"RadioButtonField" = "radio"
"ПолеПереключателя" = "radio"
"radioButton" = "radio"
"PictureField" = "picField"
"ПолеКартинки" = "picField"
"LabelField" = "labelField"
"ПолеНадписи" = "labelField"
"CalendarField" = "calendar"
"ПолеКалендаря" = "calendar"
"LabelDecoration" = "label"
"Надпись" = "label"
"PictureDecoration" = "picture"
"Картинка" = "picture"
"UsualGroup" = "group"
"Группа" = "group"
"ОбычнаяГруппа" = "group"
"ColumnGroup" = "columnGroup"
"ГруппаКолонок" = "columnGroup"
"Pages" = "pages"
"ГруппаСтраниц" = "pages"
"Page" = "page"
"Страница" = "page"
"Table" = "table"
"Таблица" = "table"
"Button" = "button"
"Кнопка" = "button"
"Popup" = "popup"
"ВсплывающееМеню" = "popup"
}
foreach ($pair in $synonyms.GetEnumerator()) {
if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) {
$val = $el.($pair.Key)
$el.PSObject.Properties.Remove($pair.Key) | Out-Null
$el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force
}
}
# Determine element type from key
$typeKey = $null
$xmlTag = $null
foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) {
foreach ($key in @("columnGroup","group","input","check","radio","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) {
if ($el.$key -ne $null) {
$typeKey = $key
break
@@ -1840,8 +1893,12 @@ function Emit-Element {
# Validate known keys — warn about typos and unknown properties
$knownKeys = @{
# type keys
"group"=1;"input"=1;"check"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1
"group"=1;"columnGroup"=1;"input"=1;"check"=1;"radio"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1
"button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1
# columnGroup-specific
"showInHeader"=1
# radio-specific
"radioButtonType"=1;"choiceList"=1;"columnsCount"=1
# naming & binding
"name"=1;"path"=1;"title"=1
# visibility & state
@@ -1851,13 +1908,15 @@ function Emit-Element {
# layout
"titleLocation"=1;"representation"=1;"width"=1;"height"=1
"horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1
"maxWidth"=1;"maxHeight"=1
# input-specific
"multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1
"spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1
"textEdit"=1
# label/hyperlink
"hyperlink"=1
# group-specific
"showTitle"=1;"united"=1
"showTitle"=1;"united"=1;"collapsed"=1
# hierarchy
"children"=1;"columns"=1
# table-specific
@@ -1885,14 +1944,16 @@ function Emit-Element {
switch ($typeKey) {
"group" { Emit-Group -el $el -name $name -id $id -indent $indent }
"columnGroup" { Emit-ColumnGroup -el $el -name $name -id $id -indent $indent }
"input" { Emit-Input -el $el -name $name -id $id -indent $indent }
"check" { Emit-Check -el $el -name $name -id $id -indent $indent }
"radio" { Emit-Radio -el $el -name $name -id $id -indent $indent }
"label" { Emit-Label -el $el -name $name -id $id -indent $indent }
"labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent }
"table" { Emit-Table -el $el -name $name -id $id -indent $indent }
"pages" { Emit-Pages -el $el -name $name -id $id -indent $indent }
"page" { Emit-Page -el $el -name $name -id $id -indent $indent }
"button" { Emit-Button -el $el -name $name -id $id -indent $indent }
"button" { Emit-Button -el $el -name $name -id $id -indent $indent -inCmdBar $inCmdBar }
"picture" { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent }
"picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent }
"calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent }
@@ -1913,10 +1974,34 @@ function Emit-CommonFlags {
if ($el.readOnly -eq $true) { X "$indent<ReadOnly>true</ReadOnly>" }
}
function Title-FromName {
param([string]$name)
if (-not $name) { return '' }
$s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2')
$s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2')
$parts = $s -split ' '
if ($parts.Count -eq 0) { return $s }
$out = New-Object System.Collections.ArrayList
[void]$out.Add($parts[0])
for ($i = 1; $i -lt $parts.Count; $i++) {
$p = $parts[$i]
if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) {
[void]$out.Add($p)
} else {
[void]$out.Add($p.ToLower())
}
}
return ($out -join ' ')
}
function Emit-Title {
param($el, [string]$name, [string]$indent)
if ($el.title) {
Emit-MLText -tag "Title" -text "$($el.title)" -indent $indent
param($el, [string]$name, [string]$indent, [switch]$auto)
$title = $el.title
if (-not $title -and $auto -and $name) {
$title = Title-FromName -name $name
}
if ($title) {
Emit-MLText -tag "Title" -text "$title" -indent $indent
}
}
@@ -1943,6 +2028,7 @@ function Emit-Group {
if ($groupVal -eq "collapsible") {
X "$inner<Group>Vertical</Group>"
X "$inner<Behavior>Collapsible</Behavior>"
if ($el.collapsed -eq $true) { X "$inner<Collapsed>true</Collapsed>" }
}
# Representation
@@ -1980,6 +2066,48 @@ function Emit-Group {
X "$indent</UsualGroup>"
}
function Emit-ColumnGroup {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<ColumnGroup name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner
# Group orientation (horizontal / vertical / inCell — последнее только здесь)
$groupVal = "$($el.columnGroup)"
$orientation = switch ($groupVal) {
"horizontal" { "Horizontal" }
"vertical" { "Vertical" }
"inCell" { "InCell" }
default { $null }
}
if ($orientation) { X "$inner<Group>$orientation</Group>" }
if ($el.showTitle -eq $false) { X "$inner<ShowTitle>false</ShowTitle>" }
if ($null -ne $el.showInHeader) {
$shVal = if ($el.showInHeader) { "true" } else { "false" }
X "$inner<ShowInHeader>$shVal</ShowInHeader>"
}
if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
Emit-CommonFlags -el $el -indent $inner
# Companion: ExtendedTooltip
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
# Children
if ($el.children -and $el.children.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($child in $el.children) {
Emit-Element -el $child -indent "$inner`t"
}
X "$inner</ChildItems>"
}
X "$indent</ColumnGroup>"
}
function Emit-Input {
param($el, [string]$name, [int]$id, [string]$indent)
@@ -1988,7 +2116,7 @@ function Emit-Input {
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.titleLocation) {
@@ -2010,9 +2138,17 @@ function Emit-Input {
if ($el.spinButton -eq $true) { X "$inner<SpinButton>true</SpinButton>" }
if ($el.dropListButton -eq $true) { X "$inner<DropListButton>true</DropListButton>" }
if ($el.markIncomplete -eq $true) { X "$inner<AutoMarkIncomplete>true</AutoMarkIncomplete>" }
if ($el.textEdit -eq $false) { X "$inner<TextEdit>false</TextEdit>" }
if ($el.skipOnInput -eq $true) { X "$inner<SkipOnInput>true</SkipOnInput>" }
if ($el.autoMaxWidth -eq $false) { X "$inner<AutoMaxWidth>false</AutoMaxWidth>" }
$hasAmw = $el.PSObject.Properties.Name -contains 'autoMaxWidth'
if ($hasAmw) {
if ($el.autoMaxWidth -eq $false) { X "$inner<AutoMaxWidth>false</AutoMaxWidth>" }
} elseif ($el.multiLine -eq $true) {
X "$inner<AutoMaxWidth>false</AutoMaxWidth>"
}
if ($null -ne $el.maxWidth) { X "$inner<MaxWidth>$($el.maxWidth)</MaxWidth>" }
if ($el.autoMaxHeight -eq $false) { X "$inner<AutoMaxHeight>false</AutoMaxHeight>" }
if ($null -ne $el.maxHeight) { X "$inner<MaxHeight>$($el.maxHeight)</MaxHeight>" }
if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
if ($el.height) { X "$inner<Height>$($el.height)</Height>" }
if ($el.horizontalStretch -eq $true) { X "$inner<HorizontalStretch>true</HorizontalStretch>" }
@@ -2039,12 +2175,11 @@ function Emit-Check {
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.titleLocation) {
X "$inner<TitleLocation>$($el.titleLocation)</TitleLocation>"
}
$tl = if ($el.titleLocation) { "$($el.titleLocation)" } else { "Right" }
X "$inner<TitleLocation>$tl</TitleLocation>"
# Companions
Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
@@ -2055,18 +2190,237 @@ function Emit-Check {
X "$indent</CheckBoxField>"
}
# Maps Russian/English root of a typed reference path to canonical English root.
# Used to normalize ChoiceList values like "Перечисление.X.Y" → "Enum.X.EnumValue.Y".
$script:refRootSynonyms = @{
"Перечисление" = "Enum"
"Справочник" = "Catalog"
"Документ" = "Document"
"ПланСчетов" = "ChartOfAccounts"
"ПланВидовХарактеристик" = "ChartOfCharacteristicTypes"
"ПланВидовРасчета" = "ChartOfCalculationTypes"
"ПланВидовРасчёта" = "ChartOfCalculationTypes"
"ПланОбмена" = "ExchangePlan"
"БизнесПроцесс" = "BusinessProcess"
"Задача" = "Task"
"РегистрСведений" = "InformationRegister"
"РегистрНакопления" = "AccumulationRegister"
"РегистрБухгалтерии" = "AccountingRegister"
"РегистрРасчета" = "CalculationRegister"
"РегистрРасчёта" = "CalculationRegister"
}
$script:enumValueSynonyms = @("EnumValue","ЗначениеПеречисления")
# Normalize a choiceList item value: returns @{ XsiType = "..."; Text = "..." }
function Normalize-ChoiceValue {
param($value)
# Booleans
if ($value -is [bool]) {
return @{ XsiType = "xs:boolean"; Text = if ($value) { "true" } else { "false" } }
}
# Numbers (int / decimal / double)
if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) {
return @{ XsiType = "xs:decimal"; Text = "$value" }
}
$s = "$value"
if ([string]::IsNullOrEmpty($s)) {
return @{ XsiType = "xs:string"; Text = "" }
}
# Try to detect typed reference path: "<Root>.<Type>[.<Member>.<Value>]"
$parts = $s -split '\.'
if ($parts.Count -ge 2) {
$root = $parts[0]
$canonRoot = $null
if ($script:refRootSynonyms.ContainsKey($root)) { $canonRoot = $script:refRootSynonyms[$root] }
elseif ($script:refRootSynonyms.Values -contains $root) { $canonRoot = $root }
if ($canonRoot) {
$typeName = $parts[1]
$normalized = $null
if ($canonRoot -eq "Enum") {
if ($parts.Count -eq 2) {
# "Enum.X" alone — not a value, treat as string
} elseif ($parts.Count -eq 3) {
# "Enum.X.Y" — insert .EnumValue.
$normalized = "Enum.$typeName.EnumValue.$($parts[2])"
} else {
# "Enum.X.<member>.Y..." — replace member with EnumValue (handles ЗначениеПеречисления too)
$member = $parts[2]
if ($script:enumValueSynonyms -contains $member) {
$rest = $parts[3..($parts.Count-1)] -join '.'
$normalized = "Enum.$typeName.EnumValue.$rest"
} else {
$rest = $parts[2..($parts.Count-1)] -join '.'
$normalized = "Enum.$typeName.EnumValue.$rest"
}
}
} else {
# Other ref roots: just translate root, keep tail as-is
if ($parts.Count -ge 3) {
$tail = $parts[1..($parts.Count-1)] -join '.'
$normalized = "$canonRoot.$tail"
}
}
if ($normalized) {
return @{ XsiType = "xr:DesignTimeRef"; Text = $normalized }
}
}
}
return @{ XsiType = "xs:string"; Text = $s }
}
# Emit Presentation block for a choiceList item.
# Accepts string (ru only), or hashtable/PSCustomObject {ru, en, ...}.
# Empty/null → emits empty <Presentation/>.
function Emit-ChoicePresentation {
param($pres, [string]$indent)
if ($null -eq $pres -or ($pres -is [string] -and [string]::IsNullOrEmpty($pres))) {
X "$indent<Presentation/>"
return
}
$pairs = @()
if ($pres -is [string]) {
$pairs += ,@("ru", $pres)
} elseif ($pres -is [hashtable] -or $pres -is [System.Collections.IDictionary]) {
foreach ($k in $pres.Keys) { $pairs += ,@("$k", "$($pres[$k])") }
} elseif ($pres.PSObject -and $pres.PSObject.Properties) {
foreach ($p in $pres.PSObject.Properties) { $pairs += ,@("$($p.Name)", "$($p.Value)") }
} else {
$pairs += ,@("ru", "$pres")
}
X "$indent<Presentation>"
foreach ($pair in $pairs) {
X "$indent`t<v8:item>"
X "$indent`t`t<v8:lang>$($pair[0])</v8:lang>"
X "$indent`t`t<v8:content>$(Esc-Xml $pair[1])</v8:content>"
X "$indent`t</v8:item>"
}
X "$indent</Presentation>"
}
function Emit-Radio {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<RadioButtonField name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
# TitleLocation default is None for radio (matches typical configurator behavior)
$tl = if ($el.titleLocation) {
switch ("$($el.titleLocation)") {
"none" { "None" }
"left" { "Left" }
"right" { "Right" }
"top" { "Top" }
"bottom" { "Bottom" }
default { "$($el.titleLocation)" }
}
} else { "None" }
X "$inner<TitleLocation>$tl</TitleLocation>"
# RadioButtonType: Auto | RadioButtons | Tumbler. Accept synonyms.
$rbtRaw = if ($el.radioButtonType) { "$($el.radioButtonType)".Trim() } else { "Auto" }
$rbt = switch -Regex ($rbtRaw.ToLower()) {
'^(auto|авто)$' { "Auto"; break }
'^(radiobuttons?|переключатель|радио)$' { "RadioButtons"; break }
'^(tumbler|тумблер)$' { "Tumbler"; break }
default { $rbtRaw }
}
X "$inner<RadioButtonType>$rbt</RadioButtonType>"
if ($null -ne $el.columnsCount) {
X "$inner<ColumnsCount>$($el.columnsCount)</ColumnsCount>"
}
# ChoiceList
if ($el.choiceList -and $el.choiceList.Count -gt 0) {
X "$inner<ChoiceList>"
$itemIndent = "$inner`t"
foreach ($item in $el.choiceList) {
# Pull value (and tolerate Russian synonym "значение")
$valRaw = $null
if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) {
if ($item.Contains("value")) { $valRaw = $item["value"] }
elseif ($item.Contains("значение")) { $valRaw = $item["значение"] }
} else {
if ($item.PSObject.Properties["value"]) { $valRaw = $item.value }
elseif ($item.PSObject.Properties["значение"]) { $valRaw = $item."значение" }
}
# Pull presentation (presentation OR title synonym)
$presRaw = $null
$hasPres = $false
if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) {
if ($item.Contains("presentation")) { $presRaw = $item["presentation"]; $hasPres = $true }
elseif ($item.Contains("представление")) { $presRaw = $item["представление"]; $hasPres = $true }
elseif ($item.Contains("title")) { $presRaw = $item["title"]; $hasPres = $true }
} else {
if ($item.PSObject.Properties["presentation"]) { $presRaw = $item.presentation; $hasPres = $true }
elseif ($item.PSObject.Properties["представление"]) { $presRaw = $item."представление"; $hasPres = $true }
elseif ($item.PSObject.Properties["title"]) { $presRaw = $item.title; $hasPres = $true }
}
$norm = Normalize-ChoiceValue -value $valRaw
# Auto-derive presentation if missing
if (-not $hasPres) {
if ($norm.XsiType -eq "xr:DesignTimeRef") {
$tail = ($norm.Text -split '\.')[-1]
$presRaw = Title-FromName -name $tail
} elseif ($norm.XsiType -eq "xs:string") {
$presRaw = $norm.Text
} else {
$presRaw = $norm.Text
}
}
X "$itemIndent<xr:Item>"
$valIndent = "$itemIndent`t"
X "$valIndent<xr:Presentation/>"
X "$valIndent<xr:CheckState>0</xr:CheckState>"
X "$valIndent<xr:Value xsi:type=`"FormChoiceListDesTimeValue`">"
Emit-ChoicePresentation -pres $presRaw -indent "$valIndent`t"
X "$valIndent`t<Value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</Value>"
X "$valIndent</xr:Value>"
X "$itemIndent</xr:Item>"
}
X "$inner</ChoiceList>"
}
# Companions
Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
Emit-Events -el $el -elementName $name -indent $inner -typeKey "radio"
X "$indent</RadioButtonField>"
}
function Emit-Label {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<LabelDecoration name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
if ($el.title) {
$labelTitle = if ($el.title) { "$($el.title)" } else { Title-FromName -name $name }
if ($labelTitle) {
$formatted = if ($el.hyperlink -eq $true) { "true" } else { "false" }
X "$inner<Title formatted=`"$formatted`">"
X "$inner`t<v8:item>"
X "$inner`t`t<v8:lang>ru</v8:lang>"
X "$inner`t`t<v8:content>$(Esc-Xml "$($el.title)")</v8:content>"
X "$inner`t`t<v8:content>$(Esc-Xml "$labelTitle")</v8:content>"
X "$inner`t</v8:item>"
X "$inner</Title>"
}
@@ -2075,7 +2429,9 @@ function Emit-Label {
if ($el.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }
if ($el.autoMaxWidth -eq $false) { X "$inner<AutoMaxWidth>false</AutoMaxWidth>" }
if ($null -ne $el.maxWidth) { X "$inner<MaxWidth>$($el.maxWidth)</MaxWidth>" }
if ($el.autoMaxHeight -eq $false) { X "$inner<AutoMaxHeight>false</AutoMaxHeight>" }
if ($null -ne $el.maxHeight) { X "$inner<MaxHeight>$($el.maxHeight)</MaxHeight>" }
if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
if ($el.height) { X "$inner<Height>$($el.height)</Height>" }
@@ -2096,7 +2452,7 @@ function Emit-LabelField {
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }
@@ -2118,7 +2474,7 @@ function Emit-Table {
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.representation) {
@@ -2207,7 +2563,7 @@ function Emit-Page {
X "$indent<Page name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto
Emit-CommonFlags -el $el -indent $inner
if ($el.group) {
@@ -2237,19 +2593,48 @@ function Emit-Page {
}
function Emit-Button {
param($el, [string]$name, [int]$id, [string]$indent)
param($el, [string]$name, [int]$id, [string]$indent, [bool]$inCmdBar = $false)
X "$indent<Button name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
# Type
# Type — context-aware:
# Inside command bar (cmdBar/autoCmdBar/popup) only CommandBarButton/CommandBarHyperlink are valid.
# UsualButton/Hyperlink would be silently ignored by 1C.
$btnType = $null
if ($el.type) {
$btnType = switch ("$($el.type)") {
"usual" { "UsualButton" }
"hyperlink" { "Hyperlink" }
"commandBar" { "CommandBarButton" }
default { "$($el.type)" }
$rawType = "$($el.type)"
if ($inCmdBar) {
# Be forgiving: any "ordinary button" hint resolves to CommandBarButton,
# any "hyperlink" hint resolves to CommandBarHyperlink. The model can pass
# either DSL ("usual"/"hyperlink") or XML names — all map to the right kind.
switch ($rawType) {
"usual" { $btnType = "CommandBarButton" }
"UsualButton" { $btnType = "CommandBarButton" }
"commandBar" { $btnType = "CommandBarButton" }
"CommandBarButton" { $btnType = "CommandBarButton" }
"hyperlink" { $btnType = "CommandBarHyperlink" }
"Hyperlink" { $btnType = "CommandBarHyperlink" }
"CommandBarHyperlink" { $btnType = "CommandBarHyperlink" }
default { $btnType = $rawType }
}
} else {
# Symmetric: any "ordinary button" hint → UsualButton, any "hyperlink" → Hyperlink.
switch ($rawType) {
"usual" { $btnType = "UsualButton" }
"UsualButton" { $btnType = "UsualButton" }
"commandBar" { $btnType = "UsualButton" }
"CommandBarButton" { $btnType = "UsualButton" }
"hyperlink" { $btnType = "Hyperlink" }
"Hyperlink" { $btnType = "Hyperlink" }
"CommandBarHyperlink" { $btnType = "Hyperlink" }
default { $btnType = $rawType }
}
}
} elseif ($inCmdBar) {
$btnType = "CommandBarButton"
}
if ($btnType) {
X "$inner<Type>$btnType</Type>"
}
@@ -2266,7 +2651,8 @@ function Emit-Button {
}
}
Emit-Title -el $el -name $name -indent $inner
$btnAuto = -not ($el.command -or $el.stdCommand)
Emit-Title -el $el -name $name -indent $inner -auto:$btnAuto
Emit-CommonFlags -el $el -indent $inner
if ($el.defaultButton -eq $true) { X "$inner<DefaultButton>true</DefaultButton>" }
@@ -2356,7 +2742,7 @@ function Emit-Calendar {
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
# Companions
@@ -2382,7 +2768,7 @@ function Emit-CommandBar {
if ($el.children -and $el.children.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($child in $el.children) {
Emit-Element -el $child -indent "$inner`t"
Emit-Element -el $child -indent "$inner`t" -inCmdBar $true
}
X "$inner</ChildItems>"
}
@@ -2396,7 +2782,7 @@ function Emit-Popup {
X "$indent<Popup name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto
Emit-CommonFlags -el $el -indent $inner
if ($el.picture) {
@@ -2414,7 +2800,7 @@ function Emit-Popup {
if ($el.children -and $el.children.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($child in $el.children) {
Emit-Element -el $child -indent "$inner`t"
Emit-Element -el $child -indent "$inner`t" -inCmdBar $true
}
X "$inner</ChildItems>"
}
@@ -2437,8 +2823,9 @@ function Emit-Attributes {
X "$indent`t<Attribute name=`"$attrName`" id=`"$attrId`">"
$inner = "$indent`t`t"
if ($attr.title) {
Emit-MLText -tag "Title" -text "$($attr.title)" -indent $inner
$attrTitle = if ($attr.title) { "$($attr.title)" } elseif ($attr.main -ne $true) { Title-FromName -name $attrName } else { '' }
if ($attrTitle) {
Emit-MLText -tag "Title" -text "$attrTitle" -indent $inner
}
# Type
@@ -2451,7 +2838,11 @@ function Emit-Attributes {
if ($attr.main -eq $true) {
X "$inner<MainAttribute>true</MainAttribute>"
}
if ($attr.savedData -eq $true) {
$mainSaved = $false
if ($attr.main -eq $true -and $attr.type) {
$mainSaved = ("$($attr.type)") -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.' -or ("$($attr.type)") -match 'RecordManager\.'
}
if ($attr.savedData -eq $true -or $mainSaved) {
X "$inner<SavedData>true</SavedData>"
}
if ($attr.fillChecking) {
@@ -2526,8 +2917,9 @@ function Emit-Commands {
X "$indent`t<Command name=`"$($cmd.name)`" id=`"$cmdId`">"
$inner = "$indent`t`t"
if ($cmd.title) {
Emit-MLText -tag "Title" -text "$($cmd.title)" -indent $inner
$cmdTitle = if ($cmd.title) { "$($cmd.title)" } else { Title-FromName -name "$($cmd.name)" }
if ($cmdTitle) {
Emit-MLText -tag "Title" -text "$cmdTitle" -indent $inner
}
if ($cmd.action) {
@@ -2601,6 +2993,176 @@ function Emit-Properties {
}
}
# --- 11b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction ---
function Normalize-ElementSynonyms {
param($el)
if ($null -eq $el) { return }
$synonyms = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" }
foreach ($pair in $synonyms.GetEnumerator()) {
if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) {
$val = $el.($pair.Key)
$el.PSObject.Properties.Remove($pair.Key) | Out-Null
$el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force
}
}
if ($el.PSObject.Properties["children"] -and $el.children) {
foreach ($child in $el.children) { Normalize-ElementSynonyms $child }
}
if ($el.PSObject.Properties["columns"] -and $el.columns) {
foreach ($child in $el.columns) { Normalize-ElementSynonyms $child }
}
}
function HasCmdBarRecursive {
param($el)
if ($null -eq $el) { return $false }
if ($el.PSObject.Properties["cmdBar"] -and $null -ne $el.cmdBar) { return $true }
if ($el.PSObject.Properties["children"] -and $el.children) {
foreach ($child in $el.children) { if (HasCmdBarRecursive $child) { return $true } }
}
if ($el.PSObject.Properties["columns"] -and $el.columns) {
foreach ($child in $el.columns) { if (HasCmdBarRecursive $child) { return $true } }
}
return $false
}
function ApplyDynamicListTableHeuristic {
param($el, [string]$listName, [bool]$hasMainTable)
if ($null -eq $el) { return }
if ($el.PSObject.Properties["table"] -and $null -ne $el.table -and "$($el.path)" -eq $listName) {
if ($null -eq $el.PSObject.Properties["tableAutofill"]) {
$el | Add-Member -NotePropertyName "tableAutofill" -NotePropertyValue $false -Force
}
if ($null -eq $el.PSObject.Properties["commandBarLocation"]) {
$el | Add-Member -NotePropertyName "commandBarLocation" -NotePropertyValue "None" -Force
}
# DefaultPicture доступен только если у DynamicList есть основная таблица
if ($hasMainTable -and ($null -eq $el.PSObject.Properties["rowPictureDataPath"] -or [string]::IsNullOrEmpty("$($el.rowPictureDataPath)"))) {
$el | Add-Member -NotePropertyName "rowPictureDataPath" -NotePropertyValue "$listName.DefaultPicture" -Force
}
}
if ($el.PSObject.Properties["children"] -and $el.children) {
foreach ($child in $el.children) { ApplyDynamicListTableHeuristic $child $listName $hasMainTable }
}
}
function Test-IsObjectLikeType {
param([string]$type)
if ([string]::IsNullOrEmpty($type)) { return $false }
if ($type -eq "DynamicList" -or $type -eq "ConstantsSet") { return $true }
$objectSuffixes = @(
"CatalogObject", "DocumentObject", "DataProcessorObject", "ReportObject",
"ExternalDataProcessorObject", "ExternalReportObject", "BusinessProcessObject",
"TaskObject", "ChartOfAccountsObject", "ChartOfCharacteristicTypesObject",
"ChartOfCalculationTypesObject", "ExchangePlanObject"
)
$recordSetPrefixes = @(
"InformationRegisterRecordSet", "AccumulationRegisterRecordSet",
"AccountingRegisterRecordSet", "CalculationRegisterRecordSet",
"InformationRegisterRecordManager"
)
foreach ($suffix in $objectSuffixes) {
if ($type -like "$suffix.*") { return $true }
}
foreach ($prefix in $recordSetPrefixes) {
if ($type -like "$prefix.*") { return $true }
}
return $false
}
# 11b.1: Normalize synonyms recursively
if ($def.elements) {
foreach ($el in $def.elements) { Normalize-ElementSynonyms $el }
}
# 11b.2: Extract autoCmdBar element from def.elements
$script:mainAcbDef = $null
if ($def.elements) {
$autoBars = @()
$rest = @()
foreach ($el in $def.elements) {
if ($null -ne $el.PSObject.Properties["autoCmdBar"] -and $null -ne $el.autoCmdBar) {
$autoBars += $el
} else {
$rest += $el
}
}
if ($autoBars.Count -gt 1) {
Write-Error "form-compile: more than one autoCmdBar in def.elements (found $($autoBars.Count)); only one allowed."
exit 1
}
if ($autoBars.Count -eq 1) {
$script:mainAcbDef = $autoBars[0]
# Replace def.elements with the filtered list
$def.PSObject.Properties.Remove("elements") | Out-Null
$def | Add-Member -NotePropertyName "elements" -NotePropertyValue $rest -Force
}
}
# 11b.3: Infer main attribute (only if no attribute has main:true)
if ($def.attributes) {
$hasExplicitMain = $false
foreach ($attr in $def.attributes) {
if ($attr.main -eq $true) { $hasExplicitMain = $true; break }
}
if (-not $hasExplicitMain) {
$candidates = @()
foreach ($attr in $def.attributes) {
# Skip if user explicitly opted out via main:false
if ($null -ne $attr.PSObject.Properties["main"] -and $attr.main -eq $false) { continue }
if (Test-IsObjectLikeType "$($attr.type)") {
$candidates += $attr
}
}
if ($candidates.Count -eq 1) {
$candidates[0] | Add-Member -NotePropertyName "main" -NotePropertyValue $true -Force
Write-Host "[INFO] Inferred main attribute: $($candidates[0].name) ($($candidates[0].type))"
} elseif ($candidates.Count -gt 1) {
$names = ($candidates | ForEach-Object { $_.name }) -join ", "
Write-Host "[WARN] Multiple main-attribute candidates: $names; specify ""main"": true explicitly"
}
}
}
# 11b.4: DynamicList → table heuristic
if ($def.attributes -and $def.elements) {
$mainAttr = $null
foreach ($attr in $def.attributes) {
if ($attr.main -eq $true) { $mainAttr = $attr; break }
}
if ($mainAttr -and "$($mainAttr.type)" -eq "DynamicList") {
$mt = $null
if ($mainAttr.PSObject.Properties["settings"] -and $null -ne $mainAttr.settings) {
if ($mainAttr.settings -is [hashtable]) {
if ($mainAttr.settings.ContainsKey("mainTable")) { $mt = $mainAttr.settings["mainTable"] }
} elseif ($mainAttr.settings.PSObject.Properties["mainTable"]) {
$mt = $mainAttr.settings.mainTable
}
}
$hasMt = -not [string]::IsNullOrEmpty("$mt")
foreach ($el in $def.elements) {
ApplyDynamicListTableHeuristic $el $mainAttr.name $hasMt
}
}
}
# 11b.5: Compute main AutoCommandBar Autofill via heuristic B3
function Compute-MainAcbAutofill {
if ($script:mainAcbDef) {
if ($null -ne $script:mainAcbDef.PSObject.Properties["autofill"]) {
return [bool]$script:mainAcbDef.autofill
}
return $true
}
if ($def.elements) {
foreach ($el in $def.elements) {
if (HasCmdBarRecursive $el) { return $false }
}
}
return $true
}
# --- 12. Main compilation ---
# Title
@@ -2632,17 +3194,26 @@ if ($formTitle) {
}
# 12b. Properties (skip 'title' — handled above as multilingual)
# When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this;
# otherwise platform appends synonym → "Title: Synonym" double-titles).
$propsClone = New-Object PSObject
$hasAutoTitle = $false
if ($def.properties) {
foreach ($p in $def.properties.PSObject.Properties) {
if ($p.Name -eq "autoTitle") { $hasAutoTitle = $true }
}
}
if ($formTitle -and -not $hasAutoTitle) {
$propsClone | Add-Member -NotePropertyName "autoTitle" -NotePropertyValue $false
}
if ($def.properties) {
$propsClone = New-Object PSObject
foreach ($p in $def.properties.PSObject.Properties) {
if ($p.Name -ne "title") {
$propsClone | Add-Member -NotePropertyName $p.Name -NotePropertyValue $p.Value
}
}
Emit-Properties -props $propsClone -indent "`t"
} else {
Emit-Properties -props $null -indent "`t"
}
Emit-Properties -props $propsClone -indent "`t"
# 12c. CommandSet (excluded commands)
if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) {
@@ -2654,10 +3225,37 @@ if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) {
}
# 12d. AutoCommandBar (always present, id=-1)
X "`t<AutoCommandBar name=`"ФормаКоманднаяПанель`" id=`"-1`">"
X "`t`t<HorizontalAlign>Right</HorizontalAlign>"
X "`t`t<Autofill>false</Autofill>"
X "`t</AutoCommandBar>"
$acbAutofill = Compute-MainAcbAutofill
$acbName = "ФормаКоманднаяПанель"
$acbHAlign = $null
if ($script:mainAcbDef) {
if ($null -ne $script:mainAcbDef.PSObject.Properties["autoCmdBar"] -and "$($script:mainAcbDef.autoCmdBar)" -ne "") {
$acbName = "$($script:mainAcbDef.autoCmdBar)"
}
if ($null -ne $script:mainAcbDef.PSObject.Properties["name"] -and "$($script:mainAcbDef.name)" -ne "") {
$acbName = "$($script:mainAcbDef.name)"
}
if ($null -ne $script:mainAcbDef.PSObject.Properties["horizontalAlign"] -and "$($script:mainAcbDef.horizontalAlign)" -ne "") {
$acbHAlign = "$($script:mainAcbDef.horizontalAlign)"
}
}
$hasAcbChildren = ($script:mainAcbDef -and $script:mainAcbDef.children -and $script:mainAcbDef.children.Count -gt 0)
$acbHasInner = ($acbHAlign -or (-not $acbAutofill) -or $hasAcbChildren)
if ($acbHasInner) {
X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`">"
if ($acbHAlign) { X "`t`t<HorizontalAlign>$acbHAlign</HorizontalAlign>" }
if (-not $acbAutofill) { X "`t`t<Autofill>false</Autofill>" }
if ($hasAcbChildren) {
X "`t`t<ChildItems>"
foreach ($child in $script:mainAcbDef.children) {
Emit-Element -el $child -indent "`t`t`t" -inCmdBar $true
}
X "`t`t</ChildItems>"
}
X "`t</AutoCommandBar>"
} else {
X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`"/>"
}
# 12e. Events
if ($def.events) {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-compile v1.6 — Compile 1C managed form from JSON or object metadata
# form-compile v1.21 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -625,6 +625,7 @@ def generate_document_list_dsl(meta, p):
table_el = OrderedDict([
('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'),
('commandBarLocation', 'None'),
('tableAutofill', False),
('columns', columns),
@@ -938,6 +939,7 @@ def generate_information_register_list_dsl(meta, p):
table_el = OrderedDict([
('table', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'),
('commandBarLocation', 'None'),
('tableAutofill', False),
('columns', columns_list),
@@ -996,6 +998,7 @@ def generate_accumulation_register_list_dsl(meta, p):
table_el = OrderedDict([
('table', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'),
('commandBarLocation', 'None'),
('tableAutofill', False),
('columns', columns_list),
@@ -1311,6 +1314,7 @@ EVENT_SUFFIX_MAP = {
KNOWN_EVENTS = {
"input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"],
"check": ["OnChange"],
"radio": ["OnChange"],
"label": ["Click", "URLProcessing"],
"labelField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "URLProcessing", "Clearing"],
"table": ["Selection", "BeforeAddRow", "AfterDeleteRow", "BeforeDeleteRow", "OnActivateRow", "OnEditEnd", "OnStartEdit", "BeforeRowChange", "BeforeEditEnd", "ValueChoice", "OnActivateCell", "OnActivateField", "Drag", "DragStart", "DragCheck", "DragEnd", "OnGetDataAtServer", "BeforeLoadUserSettingsAtServer", "OnUpdateUserSettingSetAtServer", "OnChange"],
@@ -1334,17 +1338,21 @@ KNOWN_FORM_EVENTS = [
]
KNOWN_KEYS = {
"group", "input", "check", "label", "labelField", "table", "pages", "page",
"group", "columnGroup", "input", "check", "radio", "label", "labelField", "table", "pages", "page",
"button", "picture", "picField", "calendar", "cmdBar", "popup",
"showInHeader",
"radioButtonType", "choiceList", "columnsCount",
"name", "path", "title",
"visible", "hidden", "enabled", "disabled", "readOnly", "userVisible",
"on", "handlers",
"titleLocation", "representation", "width", "height",
"horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight",
"maxWidth", "maxHeight",
"multiLine", "passwordMode", "choiceButton", "clearButton",
"spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint",
"textEdit",
"hyperlink",
"showTitle", "united",
"showTitle", "united", "collapsed",
"children", "columns",
"changeRowSet", "changeRowOrder", "header", "footer",
"commandBarLocation", "searchStringLocation",
@@ -1356,9 +1364,147 @@ KNOWN_KEYS = {
"rowPictureDataPath", "tableAutofill",
}
TYPE_KEYS = ["group", "input", "check", "label", "labelField", "table", "pages", "page",
TYPE_KEYS = ["columnGroup", "group", "input", "check", "radio", "label", "labelField", "table", "pages", "page",
"button", "picture", "picField", "calendar", "cmdBar", "popup"]
# Synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio)
ELEMENT_TYPE_SYNONYMS = {
"commandBar": "cmdBar",
"autoCommandBar": "autoCmdBar",
"КоманднаяПанель": "cmdBar",
"InputField": "input",
"ПолеВвода": "input",
"CheckBoxField": "check",
"ПолеФлажка": "check",
"RadioButtonField": "radio",
"ПолеПереключателя": "radio",
"radioButton": "radio",
"PictureField": "picField",
"ПолеКартинки": "picField",
"LabelField": "labelField",
"ПолеНадписи": "labelField",
"CalendarField": "calendar",
"ПолеКалендаря": "calendar",
"LabelDecoration": "label",
"Надпись": "label",
"PictureDecoration": "picture",
"Картинка": "picture",
"UsualGroup": "group",
"Группа": "group",
"ОбычнаяГруппа": "group",
"ColumnGroup": "columnGroup",
"ГруппаКолонок": "columnGroup",
"Pages": "pages",
"ГруппаСтраниц": "pages",
"Page": "page",
"Страница": "page",
"Table": "table",
"Таблица": "table",
"Button": "button",
"Кнопка": "button",
"Popup": "popup",
"ВсплывающееМеню": "popup",
}
# Maps Russian/English root of typed reference path to canonical English root
REF_ROOT_SYNONYMS = {
"Перечисление": "Enum",
"Справочник": "Catalog",
"Документ": "Document",
"ПланСчетов": "ChartOfAccounts",
"ПланВидовХарактеристик": "ChartOfCharacteristicTypes",
"ПланВидовРасчета": "ChartOfCalculationTypes",
"ПланВидовРасчёта": "ChartOfCalculationTypes",
"ПланОбмена": "ExchangePlan",
"БизнесПроцесс": "BusinessProcess",
"Задача": "Task",
"РегистрСведений": "InformationRegister",
"РегистрНакопления": "AccumulationRegister",
"РегистрБухгалтерии": "AccountingRegister",
"РегистрРасчета": "CalculationRegister",
"РегистрРасчёта": "CalculationRegister",
}
ENUM_VALUE_SYNONYMS = {"EnumValue", "ЗначениеПеречисления"}
def normalize_choice_value(value):
"""Returns dict {xsi_type, text} for a choiceList item value."""
if isinstance(value, bool):
return {"xsi_type": "xs:boolean", "text": "true" if value else "false"}
if isinstance(value, (int, float)):
return {"xsi_type": "xs:decimal", "text": str(value)}
s = "" if value is None else str(value)
if not s:
return {"xsi_type": "xs:string", "text": ""}
parts = s.split(".")
if len(parts) >= 2:
root = parts[0]
canon_root = None
if root in REF_ROOT_SYNONYMS:
canon_root = REF_ROOT_SYNONYMS[root]
elif root in REF_ROOT_SYNONYMS.values():
canon_root = root
if canon_root:
type_name = parts[1]
normalized = None
if canon_root == "Enum":
if len(parts) == 3:
normalized = f"Enum.{type_name}.EnumValue.{parts[2]}"
elif len(parts) >= 4:
member = parts[2]
if member in ENUM_VALUE_SYNONYMS:
rest = ".".join(parts[3:])
else:
rest = ".".join(parts[2:])
normalized = f"Enum.{type_name}.EnumValue.{rest}"
else:
if len(parts) >= 3:
tail = ".".join(parts[1:])
normalized = f"{canon_root}.{tail}"
if normalized:
return {"xsi_type": "xr:DesignTimeRef", "text": normalized}
return {"xsi_type": "xs:string", "text": s}
def emit_choice_presentation(lines, pres, indent):
"""Accepts None/empty → <Presentation/>; str → ru only; dict → multi-lang."""
if pres is None or (isinstance(pres, str) and pres == ""):
lines.append(f"{indent}<Presentation/>")
return
if isinstance(pres, str):
pairs = [("ru", pres)]
elif isinstance(pres, dict):
pairs = [(str(k), str(v)) for k, v in pres.items()]
else:
pairs = [("ru", str(pres))]
lines.append(f"{indent}<Presentation>")
for lang, content in pairs:
lines.append(f"{indent}\t<v8:item>")
lines.append(f"{indent}\t\t<v8:lang>{lang}</v8:lang>")
lines.append(f"{indent}\t\t<v8:content>{esc_xml(content)}</v8:content>")
lines.append(f"{indent}\t</v8:item>")
lines.append(f"{indent}</Presentation>")
def normalize_radio_button_type(raw):
if not raw:
return "Auto"
s = str(raw).strip().lower()
if s in ("auto", "авто"):
return "Auto"
if s in ("radiobutton", "radiobuttons", "переключатель", "радио"):
return "RadioButtons"
if s in ("tumbler", "тумблер"):
return "Tumbler"
return str(raw).strip()
def get_handler_name(element_name, event_name):
suffix = EVENT_SUFFIX_MAP.get(event_name)
@@ -1414,9 +1560,27 @@ def emit_common_flags(lines, el, indent):
lines.append(f"{indent}<ReadOnly>true</ReadOnly>")
def emit_title(lines, el, name, indent):
if el.get('title'):
emit_mltext(lines, indent, 'Title', str(el['title']))
def title_from_name(name):
"""СуммаДокумента → 'Сумма документа'. НДСВключен → 'НДС включен'."""
if not name:
return ''
s = re.sub(r'([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', r'\1 \2', name)
s = re.sub(r'([а-яa-z0-9])([А-ЯA-Z])', r'\1 \2', s)
parts = s.split(' ')
if not parts:
return s
out = [parts[0]]
for p in parts[1:]:
out.append(p if (len(p) > 1 and p.isupper()) else p.lower())
return ' '.join(out)
def emit_title(lines, el, name, indent, auto=False):
title = el.get('title')
if not title and auto and name:
title = title_from_name(name)
if title:
emit_mltext(lines, indent, 'Title', str(title))
# --- Type emitter ---
@@ -1455,7 +1619,7 @@ CFG_REF_PATTERN = re.compile(
)
KNOWN_INVALID_TYPES = {
'FormDataStructure': 'Runtime type. Use cfg:*Object.XXX (e.g. CatalogObject.XXX)',
'FormDataStructure': 'Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)',
'FormDataCollection': 'Runtime type. Use ValueTable',
'FormDataTree': 'Runtime type. Use ValueTree',
'FormDataTreeItem': 'Runtime type, not valid in XML',
@@ -1489,6 +1653,9 @@ _FORM_TYPE_SYNONYMS = {
def resolve_type_str(type_str):
if not type_str:
return type_str
# Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix)
if type_str.startswith('cfg:'):
type_str = type_str[4:]
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
if m:
base, params = m.group(1).strip(), m.group(2)
@@ -1598,7 +1765,12 @@ def emit_type(lines, type_str, indent):
# --- Element emitters ---
def emit_element(lines, el, indent):
def emit_element(lines, el, indent, in_cmd_bar=False):
# Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio)
for src, dst in ELEMENT_TYPE_SYNONYMS.items():
if src in el and dst not in el:
el[dst] = el.pop(src)
type_key = None
for key in TYPE_KEYS:
if el.get(key) is not None:
@@ -1619,8 +1791,10 @@ def emit_element(lines, el, indent):
emitters = {
'group': emit_group,
'columnGroup': emit_column_group,
'input': emit_input,
'check': emit_check,
'radio': emit_radio_button_field,
'label': emit_label,
'labelField': emit_label_field,
'table': emit_table,
@@ -1636,7 +1810,10 @@ def emit_element(lines, el, indent):
emitter = emitters.get(type_key)
if emitter:
emitter(lines, el, name, eid, indent)
if type_key == 'button':
emitter(lines, el, name, eid, indent, in_cmd_bar=in_cmd_bar)
else:
emitter(lines, el, name, eid, indent)
def emit_group(lines, el, name, eid, indent):
@@ -1661,6 +1838,8 @@ def emit_group(lines, el, name, eid, indent):
if group_val == 'collapsible':
lines.append(f'{inner}<Group>Vertical</Group>')
lines.append(f'{inner}<Behavior>Collapsible</Behavior>')
if el.get('collapsed') is True:
lines.append(f'{inner}<Collapsed>true</Collapsed>')
# Representation
if el.get('representation'):
@@ -1696,6 +1875,43 @@ def emit_group(lines, el, name, eid, indent):
lines.append(f'{indent}</UsualGroup>')
def emit_column_group(lines, el, name, eid, indent):
lines.append(f'{indent}<ColumnGroup name="{name}" id="{eid}">')
inner = f'{indent}\t'
emit_title(lines, el, name, inner)
group_val = str(el.get('columnGroup', ''))
orientation_map = {
'horizontal': 'Horizontal',
'vertical': 'Vertical',
'inCell': 'InCell',
}
orientation = orientation_map.get(group_val)
if orientation:
lines.append(f'{inner}<Group>{orientation}</Group>')
if el.get('showTitle') is False:
lines.append(f'{inner}<ShowTitle>false</ShowTitle>')
if el.get('showInHeader') is not None:
sh_val = 'true' if el['showInHeader'] else 'false'
lines.append(f'{inner}<ShowInHeader>{sh_val}</ShowInHeader>')
if el.get('width'):
lines.append(f'{inner}<Width>{el["width"]}</Width>')
emit_common_flags(lines, el, inner)
emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner)
if el.get('children') and len(el['children']) > 0:
lines.append(f'{inner}<ChildItems>')
for child in el['children']:
emit_element(lines, child, f'{inner}\t')
lines.append(f'{inner}</ChildItems>')
lines.append(f'{indent}</ColumnGroup>')
def emit_input(lines, el, name, eid, indent):
lines.append(f'{indent}<InputField name="{name}" id="{eid}">')
inner = f'{indent}\t'
@@ -1703,7 +1919,7 @@ def emit_input(lines, el, name, eid, indent):
if el.get('path'):
lines.append(f'{inner}<DataPath>{el["path"]}</DataPath>')
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=not el.get('path'))
emit_common_flags(lines, el, inner)
if el.get('titleLocation'):
@@ -1725,12 +1941,21 @@ def emit_input(lines, el, name, eid, indent):
lines.append(f'{inner}<DropListButton>true</DropListButton>')
if el.get('markIncomplete') is True:
lines.append(f'{inner}<AutoMarkIncomplete>true</AutoMarkIncomplete>')
if el.get('textEdit') is False:
lines.append(f'{inner}<TextEdit>false</TextEdit>')
if el.get('skipOnInput') is True:
lines.append(f'{inner}<SkipOnInput>true</SkipOnInput>')
if el.get('autoMaxWidth') is False:
if 'autoMaxWidth' in el:
if el['autoMaxWidth'] is False:
lines.append(f'{inner}<AutoMaxWidth>false</AutoMaxWidth>')
elif el.get('multiLine') is True:
lines.append(f'{inner}<AutoMaxWidth>false</AutoMaxWidth>')
if el.get('maxWidth') is not None:
lines.append(f'{inner}<MaxWidth>{el["maxWidth"]}</MaxWidth>')
if el.get('autoMaxHeight') is False:
lines.append(f'{inner}<AutoMaxHeight>false</AutoMaxHeight>')
if el.get('maxHeight') is not None:
lines.append(f'{inner}<MaxHeight>{el["maxHeight"]}</MaxHeight>')
if el.get('width'):
lines.append(f'{inner}<Width>{el["width"]}</Width>')
if el.get('height'):
@@ -1759,11 +1984,11 @@ def emit_check(lines, el, name, eid, indent):
if el.get('path'):
lines.append(f'{inner}<DataPath>{el["path"]}</DataPath>')
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=not el.get('path'))
emit_common_flags(lines, el, inner)
if el.get('titleLocation'):
lines.append(f'{inner}<TitleLocation>{el["titleLocation"]}</TitleLocation>')
tl = el.get('titleLocation') or 'Right'
lines.append(f'{inner}<TitleLocation>{tl}</TitleLocation>')
# Companions
emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
@@ -1774,16 +1999,80 @@ def emit_check(lines, el, name, eid, indent):
lines.append(f'{indent}</CheckBoxField>')
def emit_radio_button_field(lines, el, name, eid, indent):
lines.append(f'{indent}<RadioButtonField name="{name}" id="{eid}">')
inner = f'{indent}\t'
if el.get('path'):
lines.append(f'{inner}<DataPath>{el["path"]}</DataPath>')
emit_title(lines, el, name, inner, auto=not el.get('path'))
emit_common_flags(lines, el, inner)
tl_raw = el.get('titleLocation')
if tl_raw:
loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom'}
tl = loc_map.get(str(tl_raw), str(tl_raw))
else:
tl = 'None'
lines.append(f'{inner}<TitleLocation>{tl}</TitleLocation>')
rbt = normalize_radio_button_type(el.get('radioButtonType'))
lines.append(f'{inner}<RadioButtonType>{rbt}</RadioButtonType>')
if el.get('columnsCount') is not None:
lines.append(f'{inner}<ColumnsCount>{el["columnsCount"]}</ColumnsCount>')
choice_list = el.get('choiceList') or []
if choice_list:
lines.append(f'{inner}<ChoiceList>')
item_indent = f'{inner}\t'
for item in choice_list:
if not isinstance(item, dict):
continue
val_raw = item.get('value', item.get('значение'))
has_pres = any(k in item for k in ('presentation', 'представление', 'title'))
pres_raw = item.get('presentation', item.get('представление', item.get('title')))
norm = normalize_choice_value(val_raw)
if not has_pres:
if norm['xsi_type'] == 'xr:DesignTimeRef':
tail = norm['text'].split('.')[-1]
pres_raw = title_from_name(tail)
else:
pres_raw = norm['text']
lines.append(f'{item_indent}<xr:Item>')
val_indent = f'{item_indent}\t'
lines.append(f'{val_indent}<xr:Presentation/>')
lines.append(f'{val_indent}<xr:CheckState>0</xr:CheckState>')
lines.append(f'{val_indent}<xr:Value xsi:type="FormChoiceListDesTimeValue">')
emit_choice_presentation(lines, pres_raw, f'{val_indent}\t')
lines.append(f'{val_indent}\t<Value xsi:type="{norm["xsi_type"]}">{esc_xml(norm["text"])}</Value>')
lines.append(f'{val_indent}</xr:Value>')
lines.append(f'{item_indent}</xr:Item>')
lines.append(f'{inner}</ChoiceList>')
emit_companion(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner)
emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner)
emit_events(lines, el, name, inner, 'radio')
lines.append(f'{indent}</RadioButtonField>')
def emit_label(lines, el, name, eid, indent):
lines.append(f'{indent}<LabelDecoration name="{name}" id="{eid}">')
inner = f'{indent}\t'
if el.get('title'):
label_title = el.get('title') or title_from_name(name)
if label_title:
formatted = 'true' if el.get('hyperlink') is True else 'false'
lines.append(f'{inner}<Title formatted="{formatted}">')
lines.append(f'{inner}\t<v8:item>')
lines.append(f'{inner}\t\t<v8:lang>ru</v8:lang>')
lines.append(f'{inner}\t\t<v8:content>{esc_xml(str(el["title"]))}</v8:content>')
lines.append(f'{inner}\t\t<v8:content>{esc_xml(str(label_title))}</v8:content>')
lines.append(f'{inner}\t</v8:item>')
lines.append(f'{inner}</Title>')
@@ -1793,8 +2082,12 @@ def emit_label(lines, el, name, eid, indent):
lines.append(f'{inner}<Hyperlink>true</Hyperlink>')
if el.get('autoMaxWidth') is False:
lines.append(f'{inner}<AutoMaxWidth>false</AutoMaxWidth>')
if el.get('maxWidth') is not None:
lines.append(f'{inner}<MaxWidth>{el["maxWidth"]}</MaxWidth>')
if el.get('autoMaxHeight') is False:
lines.append(f'{inner}<AutoMaxHeight>false</AutoMaxHeight>')
if el.get('maxHeight') is not None:
lines.append(f'{inner}<MaxHeight>{el["maxHeight"]}</MaxHeight>')
if el.get('width'):
lines.append(f'{inner}<Width>{el["width"]}</Width>')
if el.get('height'):
@@ -1816,7 +2109,7 @@ def emit_label_field(lines, el, name, eid, indent):
if el.get('path'):
lines.append(f'{inner}<DataPath>{el["path"]}</DataPath>')
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=not el.get('path'))
emit_common_flags(lines, el, inner)
if el.get('hyperlink') is True:
@@ -1838,7 +2131,7 @@ def emit_table(lines, el, name, eid, indent):
if el.get('path'):
lines.append(f'{inner}<DataPath>{el["path"]}</DataPath>')
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=not el.get('path'))
emit_common_flags(lines, el, inner)
if el.get('representation'):
@@ -1926,7 +2219,7 @@ def emit_page(lines, el, name, eid, indent):
lines.append(f'{indent}<Page name="{name}" id="{eid}">')
inner = f'{indent}\t'
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=True)
emit_common_flags(lines, el, inner)
if el.get('group'):
@@ -1953,14 +2246,42 @@ def emit_page(lines, el, name, eid, indent):
lines.append(f'{indent}</Page>')
def emit_button(lines, el, name, eid, indent):
def emit_button(lines, el, name, eid, indent, in_cmd_bar=False):
lines.append(f'{indent}<Button name="{name}" id="{eid}">')
inner = f'{indent}\t'
# Type
# Type — context-aware. Inside command bars (cmdBar/autoCmdBar/popup) only
# CommandBarButton/CommandBarHyperlink are valid; UsualButton/Hyperlink would be ignored.
# Forgiving resolver: any "ordinary button" hint resolves to UsualButton/CommandBarButton,
# any "hyperlink" hint resolves to Hyperlink/CommandBarHyperlink — depending on context.
btn_type = None
if el.get('type'):
btn_type_map = {'usual': 'UsualButton', 'hyperlink': 'Hyperlink', 'commandBar': 'CommandBarButton'}
btn_type = btn_type_map.get(str(el['type']), str(el['type']))
raw = str(el['type'])
if in_cmd_bar:
cmd_bar_map = {
'usual': 'CommandBarButton',
'UsualButton': 'CommandBarButton',
'commandBar': 'CommandBarButton',
'CommandBarButton': 'CommandBarButton',
'hyperlink': 'CommandBarHyperlink',
'Hyperlink': 'CommandBarHyperlink',
'CommandBarHyperlink': 'CommandBarHyperlink',
}
btn_type = cmd_bar_map.get(raw, raw)
else:
normal_map = {
'usual': 'UsualButton',
'UsualButton': 'UsualButton',
'commandBar': 'UsualButton',
'CommandBarButton': 'UsualButton',
'hyperlink': 'Hyperlink',
'Hyperlink': 'Hyperlink',
'CommandBarHyperlink': 'Hyperlink',
}
btn_type = normal_map.get(raw, raw)
elif in_cmd_bar:
btn_type = 'CommandBarButton'
if btn_type:
lines.append(f'{inner}<Type>{btn_type}</Type>')
# CommandName
@@ -1974,7 +2295,7 @@ def emit_button(lines, el, name, eid, indent):
else:
lines.append(f'{inner}<CommandName>Form.StandardCommand.{sc}</CommandName>')
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=not (el.get('command') or el.get('stdCommand')))
emit_common_flags(lines, el, inner)
if el.get('defaultButton') is True:
@@ -2062,7 +2383,7 @@ def emit_calendar(lines, el, name, eid, indent):
if el.get('path'):
lines.append(f'{inner}<DataPath>{el["path"]}</DataPath>')
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=not el.get('path'))
emit_common_flags(lines, el, inner)
# Companions
@@ -2087,7 +2408,7 @@ def emit_command_bar(lines, el, name, eid, indent):
if el.get('children') and len(el['children']) > 0:
lines.append(f'{inner}<ChildItems>')
for child in el['children']:
emit_element(lines, child, f'{inner}\t')
emit_element(lines, child, f'{inner}\t', in_cmd_bar=True)
lines.append(f'{inner}</ChildItems>')
lines.append(f'{indent}</CommandBar>')
@@ -2097,7 +2418,7 @@ def emit_popup(lines, el, name, eid, indent):
lines.append(f'{indent}<Popup name="{name}" id="{eid}">')
inner = f'{indent}\t'
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=True)
emit_common_flags(lines, el, inner)
if el.get('picture'):
@@ -2113,7 +2434,7 @@ def emit_popup(lines, el, name, eid, indent):
if el.get('children') and len(el['children']) > 0:
lines.append(f'{inner}<ChildItems>')
for child in el['children']:
emit_element(lines, child, f'{inner}\t')
emit_element(lines, child, f'{inner}\t', in_cmd_bar=True)
lines.append(f'{inner}</ChildItems>')
lines.append(f'{indent}</Popup>')
@@ -2133,8 +2454,11 @@ def emit_attributes(lines, attrs, indent):
lines.append(f'{indent}\t<Attribute name="{attr_name}" id="{attr_id}">')
inner = f'{indent}\t\t'
if attr.get('title'):
emit_mltext(lines, inner, 'Title', str(attr['title']))
attr_title = attr.get('title')
if not attr_title and attr.get('main') is not True:
attr_title = title_from_name(attr_name)
if attr_title:
emit_mltext(lines, inner, 'Title', str(attr_title))
# Type
if attr.get('type'):
@@ -2144,7 +2468,11 @@ def emit_attributes(lines, attrs, indent):
if attr.get('main') is True:
lines.append(f'{inner}<MainAttribute>true</MainAttribute>')
if attr.get('savedData') is True:
main_saved = False
if attr.get('main') is True and attr.get('type'):
t = str(attr['type'])
main_saved = bool(re.match(r'^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.', t)) or ('RecordManager.' in t)
if attr.get('savedData') is True or main_saved:
lines.append(f'{inner}<SavedData>true</SavedData>')
if attr.get('fillChecking'):
lines.append(f'{inner}<FillChecking>{attr["fillChecking"]}</FillChecking>')
@@ -2210,8 +2538,9 @@ def emit_commands(lines, cmds, indent):
lines.append(f'{indent}\t<Command name="{cmd["name"]}" id="{cmd_id}">')
inner = f'{indent}\t\t'
if cmd.get('title'):
emit_mltext(lines, inner, 'Title', str(cmd['title']))
cmd_title = cmd.get('title') or title_from_name(str(cmd['name']))
if cmd_title:
emit_mltext(lines, inner, 'Title', str(cmd_title))
if cmd.get('action'):
lines.append(f'{inner}<Action>{cmd["action"]}</Action>')
@@ -2533,6 +2862,131 @@ def main():
with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = json.load(f)
# --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction ---
def _normalize_synonyms(el):
if not isinstance(el, dict):
return
synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar'}
for src, dst in synonyms.items():
if src in el and dst not in el:
el[dst] = el.pop(src)
if isinstance(el.get('children'), list):
for child in el['children']:
_normalize_synonyms(child)
if isinstance(el.get('columns'), list):
for child in el['columns']:
_normalize_synonyms(child)
def _has_cmd_bar_recursive(el):
if not isinstance(el, dict):
return False
if el.get('cmdBar') is not None:
return True
if isinstance(el.get('children'), list):
for child in el['children']:
if _has_cmd_bar_recursive(child):
return True
if isinstance(el.get('columns'), list):
for child in el['columns']:
if _has_cmd_bar_recursive(child):
return True
return False
def _apply_dlist_table_heuristic(el, list_name, has_main_table):
if not isinstance(el, dict):
return
if el.get('table') is not None and str(el.get('path', '')) == list_name:
if 'tableAutofill' not in el:
el['tableAutofill'] = False
if 'commandBarLocation' not in el:
el['commandBarLocation'] = 'None'
# DefaultPicture доступен только если у DynamicList есть основная таблица
if has_main_table and not el.get('rowPictureDataPath'):
el['rowPictureDataPath'] = f'{list_name}.DefaultPicture'
if isinstance(el.get('children'), list):
for child in el['children']:
_apply_dlist_table_heuristic(child, list_name, has_main_table)
def _is_object_like_type(t):
if not t:
return False
if t == 'DynamicList' or t == 'ConstantsSet':
return True
object_suffixes = (
'CatalogObject', 'DocumentObject', 'DataProcessorObject', 'ReportObject',
'ExternalDataProcessorObject', 'ExternalReportObject', 'BusinessProcessObject',
'TaskObject', 'ChartOfAccountsObject', 'ChartOfCharacteristicTypesObject',
'ChartOfCalculationTypesObject', 'ExchangePlanObject',
)
record_set_prefixes = (
'InformationRegisterRecordSet', 'AccumulationRegisterRecordSet',
'AccountingRegisterRecordSet', 'CalculationRegisterRecordSet',
'InformationRegisterRecordManager',
)
for s in object_suffixes:
if t.startswith(s + '.'):
return True
for s in record_set_prefixes:
if t.startswith(s + '.'):
return True
return False
# 1b.1: Normalize synonyms recursively
if isinstance(defn.get('elements'), list):
for el in defn['elements']:
_normalize_synonyms(el)
# 1b.2: Extract autoCmdBar element from defn['elements']
main_acb_def = None
if isinstance(defn.get('elements'), list):
auto_bars = [el for el in defn['elements'] if isinstance(el, dict) and el.get('autoCmdBar') is not None]
if len(auto_bars) > 1:
print(f"form-compile: more than one autoCmdBar in def.elements (found {len(auto_bars)}); only one allowed.", file=sys.stderr)
sys.exit(1)
if len(auto_bars) == 1:
main_acb_def = auto_bars[0]
defn['elements'] = [el for el in defn['elements'] if el is not main_acb_def]
# 1b.3: Infer main attribute
if isinstance(defn.get('attributes'), list):
has_explicit_main = any(a.get('main') is True for a in defn['attributes'] if isinstance(a, dict))
if not has_explicit_main:
candidates = []
for a in defn['attributes']:
if not isinstance(a, dict):
continue
if 'main' in a and a.get('main') is False:
continue
if _is_object_like_type(str(a.get('type', ''))):
candidates.append(a)
if len(candidates) == 1:
candidates[0]['main'] = True
print(f"[INFO] Inferred main attribute: {candidates[0].get('name')} ({candidates[0].get('type')})")
elif len(candidates) > 1:
names = ', '.join(c.get('name', '') for c in candidates)
print(f"[WARN] Multiple main-attribute candidates: {names}; specify \"main\": true explicitly")
# 1b.4: DynamicList → table heuristic
if isinstance(defn.get('attributes'), list) and isinstance(defn.get('elements'), list):
main_attr = next((a for a in defn['attributes'] if isinstance(a, dict) and a.get('main') is True), None)
if main_attr and str(main_attr.get('type', '')) == 'DynamicList':
settings = main_attr.get('settings') or {}
has_mt = bool(isinstance(settings, dict) and settings.get('mainTable'))
for el in defn['elements']:
_apply_dlist_table_heuristic(el, main_attr.get('name', ''), has_mt)
# 1b.5: Compute main AutoCommandBar Autofill (B3)
def _compute_main_acb_autofill():
if main_acb_def is not None:
if 'autofill' in main_acb_def:
return bool(main_acb_def.get('autofill'))
return True
if isinstance(defn.get('elements'), list):
for el in defn['elements']:
if _has_cmd_bar_recursive(el):
return False
return True
# --- 2. Main compilation ---
_next_id = 0
lines = []
@@ -2548,9 +3002,16 @@ def main():
emit_mltext(lines, '\t', 'Title', str(form_title))
# Properties (skip 'title' — handled above)
if defn.get('properties'):
props_clone = {k: v for k, v in defn['properties'].items() if k != 'title'}
emit_properties(lines, props_clone, '\t')
# When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this;
# otherwise platform appends synonym → "Title: Synonym" double-titles).
props_src = defn.get('properties') or {}
props_clone = OrderedDict()
if form_title and 'autoTitle' not in props_src:
props_clone['autoTitle'] = False
for k, v in props_src.items():
if k != 'title':
props_clone[k] = v
emit_properties(lines, props_clone, '\t')
# CommandSet (excluded commands)
if defn.get('excludedCommands') and len(defn['excludedCommands']) > 0:
@@ -2560,10 +3021,33 @@ def main():
lines.append('\t</CommandSet>')
# AutoCommandBar (always present, id=-1)
lines.append('\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">')
lines.append('\t\t<HorizontalAlign>Right</HorizontalAlign>')
lines.append('\t\t<Autofill>false</Autofill>')
lines.append('\t</AutoCommandBar>')
acb_autofill = _compute_main_acb_autofill()
acb_name = '\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c'
acb_halign = None
if main_acb_def is not None:
v = main_acb_def.get('autoCmdBar')
if v:
acb_name = str(v)
if main_acb_def.get('name'):
acb_name = str(main_acb_def['name'])
if main_acb_def.get('horizontalAlign'):
acb_halign = str(main_acb_def['horizontalAlign'])
has_acb_children = bool(main_acb_def and isinstance(main_acb_def.get('children'), list) and len(main_acb_def['children']) > 0)
has_inner = bool(acb_halign) or (not acb_autofill) or has_acb_children
if has_inner:
lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1">')
if acb_halign:
lines.append(f'\t\t<HorizontalAlign>{acb_halign}</HorizontalAlign>')
if not acb_autofill:
lines.append('\t\t<Autofill>false</Autofill>')
if has_acb_children:
lines.append('\t\t<ChildItems>')
for child in main_acb_def['children']:
emit_element(lines, child, '\t\t\t', in_cmd_bar=True)
lines.append('\t\t</ChildItems>')
lines.append('\t</AutoCommandBar>')
else:
lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1"/>')
# Events
if defn.get('events'):
+1 -1
View File
@@ -29,7 +29,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/form-edit/scripts/form-edit.ps1 -FormPath "<путь>" -JsonPath "<путь>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-edit.ps1" -FormPath "<путь>" -JsonPath "<путь>"
```
## JSON формат
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$FormPath,
[Parameter(Mandatory)]
@@ -14,7 +14,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-FormPath", required=True)
parser.add_argument("-FormPath", "-Path", required=True)
parser.add_argument("-JsonPath", required=True)
args = parser.parse_args()
+1 -1
View File
@@ -15,7 +15,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/form-info/scripts/form-info.ps1 -FormPath "<путь к Form.xml>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-info.ps1" -FormPath "<путь к Form.xml>"
```
## Параметры
+64 -1
View File
@@ -1,7 +1,8 @@
# form-info v1.2 — Analyze 1C managed form structure
# form-info v1.3 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)]
[Alias('Path')]
[string]$FormPath,
[int]$Limit = 150,
[int]$Offset = 0,
@@ -435,6 +436,62 @@ if ($formEvents -and $formEvents.HasChildNodes) {
}
}
# --- Main AutoCommandBar (form's id=-1 panel) ---
function Format-MainAcb($acbNode) {
if (-not $acbNode) { return @() }
$result = @()
$autofillNode = $acbNode.SelectSingleNode("d:Autofill", $ns)
$autofill = $true
if ($autofillNode -and $autofillNode.InnerText -eq "false") { $autofill = $false }
$halignNode = $acbNode.SelectSingleNode("d:HorizontalAlign", $ns)
$flags = @()
$flags += if ($autofill) { "autofill" } else { "no-autofill" }
if ($halignNode) { $flags += "align=$($halignNode.InnerText)" }
$header = "AutoCommandBar [$($flags -join ', ')]"
$childItemsNode = $acbNode.SelectSingleNode("d:ChildItems", $ns)
$buttons = @()
if ($childItemsNode) {
foreach ($btn in $childItemsNode.ChildNodes) {
if ($btn.NodeType -ne "Element") { continue }
if ($skipElements.ContainsKey($btn.LocalName)) { continue }
$bName = $btn.GetAttribute("name")
$cmdNode = $btn.SelectSingleNode("d:CommandName", $ns)
$cmdRef = if ($cmdNode) { $cmdNode.InnerText } else { "" }
$locNode = $btn.SelectSingleNode("d:LocationInCommandBar", $ns)
$locStr = if ($locNode) { " [$($locNode.InnerText)]" } else { "" }
$tag = Get-ElementTag $btn
if ($cmdRef) {
$buttons += " $tag $bName -> $cmdRef$locStr"
} else {
$buttons += " $tag $bName$locStr"
}
}
}
if ($buttons.Count -eq 0 -and $autofill -and -not $halignNode) {
# Default empty panel — terse one-liner
return @("AutoCommandBar [autofill]")
}
$result += $header
$result += $buttons
return $result
}
# Determine position from CommandBarLocation form property
$cbLocNode = $root.SelectSingleNode("d:CommandBarLocation", $ns)
$cbLoc = if ($cbLocNode) { $cbLocNode.InnerText } else { "Auto" }
$mainAcbNode = $root.SelectSingleNode("d:AutoCommandBar", $ns)
$acbLines = @()
if ($cbLoc -ne "None" -and $mainAcbNode) {
$acbLines = Format-MainAcb $mainAcbNode
}
# AutoCommandBar above Elements (Auto/Top)
if ($acbLines.Count -gt 0 -and ($cbLoc -eq "Auto" -or $cbLoc -eq "Top")) {
$lines += ""
$lines += $acbLines
}
# --- Element tree ---
$childItems = $root.SelectSingleNode("d:ChildItems", $ns)
@@ -445,6 +502,12 @@ if ($childItems) {
$lines += $treeLines.ToArray()
}
# AutoCommandBar below Elements (Bottom)
if ($acbLines.Count -gt 0 -and $cbLoc -eq "Bottom") {
$lines += ""
$lines += $acbLines
}
# --- Attributes ---
$attrsNode = $root.SelectSingleNode("d:Attributes", $ns)
+50 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-info v1.2 — Analyze 1C managed form structure
# form-info v1.3 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -342,7 +342,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Analyze 1C managed form structure", allow_abbrev=False)
parser.add_argument("-FormPath", required=True, help="Path to Form.xml")
parser.add_argument("-FormPath", "-Path", required=True, help="Path to Form.xml")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Line offset for pagination")
parser.add_argument("-Expand", default="", help="Expand collapsed section by name, or * for all")
@@ -484,6 +484,50 @@ def main():
ct_str = f"[{ct}]" if ct else ""
lines.append(f" {e_name}{ct_str} -> {e_handler}")
# --- Main AutoCommandBar (form's id=-1 panel) ---
def format_main_acb(acb_node):
if acb_node is None:
return []
autofill_node = acb_node.find("d:Autofill", NSMAP)
autofill = not (autofill_node is not None and autofill_node.text == "false")
halign_node = acb_node.find("d:HorizontalAlign", NSMAP)
flags = ["autofill" if autofill else "no-autofill"]
if halign_node is not None and halign_node.text:
flags.append(f"align={halign_node.text}")
ci_node = acb_node.find("d:ChildItems", NSMAP)
buttons = []
if ci_node is not None:
for btn in ci_node:
if not isinstance(btn.tag, str):
continue
ln = etree.QName(btn).localname
if ln in SKIP_ELEMENTS:
continue
b_name = btn.get("name", "")
cmd_node = btn.find("d:CommandName", NSMAP)
cmd_ref = cmd_node.text if cmd_node is not None and cmd_node.text else ""
loc_node = btn.find("d:LocationInCommandBar", NSMAP)
loc_str = f" [{loc_node.text}]" if loc_node is not None and loc_node.text else ""
tag = get_element_tag(btn)
if cmd_ref:
buttons.append(f" {tag} {b_name} -> {cmd_ref}{loc_str}")
else:
buttons.append(f" {tag} {b_name}{loc_str}")
if not buttons and autofill and halign_node is None:
return ["AutoCommandBar [autofill]"]
return [f"AutoCommandBar [{', '.join(flags)}]"] + buttons
cb_loc_node = root.find("d:CommandBarLocation", NSMAP)
cb_loc = cb_loc_node.text if cb_loc_node is not None and cb_loc_node.text else "Auto"
main_acb_node = root.find("d:AutoCommandBar", NSMAP)
acb_lines = []
if cb_loc != "None" and main_acb_node is not None:
acb_lines = format_main_acb(main_acb_node)
if acb_lines and cb_loc in ("Auto", "Top"):
lines.append("")
lines.extend(acb_lines)
# --- Element tree ---
tree_state = {"has_collapsed": False}
child_items = root.find("d:ChildItems", NSMAP)
@@ -494,6 +538,10 @@ def main():
build_tree(child_items, " ", tree_lines, expand, tree_state)
lines.extend(tree_lines)
if acb_lines and cb_loc == "Bottom":
lines.append("")
lines.extend(acb_lines)
# --- Attributes ---
attrs_node = root.find("d:Attributes", NSMAP)
if attrs_node is not None:
+11 -11
View File
@@ -41,9 +41,9 @@ allowed-tools: []
├─ Информационные надписи (label, hyperlink)
Рабочая область
├─ Таблица данных или Pages с вкладками
Кнопки действий
├─ Выполнить / Применить (defaultButton)
─ Закрыть (stdCommand: Close)
Главная АКП формы (autoCmdBar)
├─ Выполнить / Применить (defaultButton: true)
─ Закрыть (stdCommand: Close)
```
**События:** OnCreateAtServer, OnOpen, NotificationProcessing
@@ -90,12 +90,12 @@ allowed-tools: []
├─ Шаг1: описание + параметры
├─ Шаг2: основная работа
└─ Шаг3: результат
Кнопки (horizontal)
├─ Назад (command), Далее (command, defaultButton), Выполнить (command)
Главная АКП формы (autoCmdBar)
├─ Назад, Далее (defaultButton: true), Выполнить
└─ Закрыть (stdCommand: Close)
```
**Свойства:** windowOpeningMode=LockOwnerWindow, commandBarLocation=None
**Свойства:** windowOpeningMode=LockOwnerWindow
---
@@ -111,7 +111,7 @@ allowed-tools: []
| Номер+Дата | `ГруппаНомерДата` | horizontal |
| Подвал | `ГруппаПодвал` | vertical |
| Итоги | `ГруппаИтоги` | horizontal |
| Кнопки | `ГруппаКнопок` | horizontal |
| Главная АКП формы | `ФормаКоманднаяПанель` | autoCmdBar |
| Страницы | `ГруппаСтраницы` / `Страницы` | pages |
| Предупреждение | `ГруппаПредупреждение` | horizontal, visible:false |
| Доп. секция | `ГруппаДополнительно` / `ГруппаПрочее` | vertical, collapse |
@@ -150,7 +150,7 @@ allowed-tools: []
1. **Порядок чтения.** Сверху вниз, слева направо. Самое важное — вверху.
2. **Двухколоночная шапка.** Основные реквизиты слева (контрагент, склад), организационные справа (организация, подразделение).
3. **Кнопки действий внизу.** Главная кнопка — `defaultButton: true`. Закрыть — всегда последняя.
3. **Кнопки действий — на главной АКП формы** (`autoCmdBar`), не в отдельной группе на форме. Главная кнопка — `defaultButton: true`. Закрыть — всегда последняя.
4. **Таблицы — основная область.** Табличные части занимают большую часть формы, обычно на Pages.
5. **Итоги рядом с таблицей.** В подвале, горизонтальная группа, все поля readOnly.
6. **Фильтры — отдельная зона.** Над списком, alwaysHorizontal, пара «флажок + поле» на каждый фильтр.
@@ -166,8 +166,8 @@ allowed-tools: []
Для необязательных секций (подписи, дополнительно, прочее):
```json
{ "group": "vertical", "name": "ГруппаПодписи", "title": "Подписи",
"behavior": "Collapsible", "collapsed": true, "children": [...] }
{ "group": "collapsible", "name": "ГруппаПодписи", "title": "Подписи",
"collapsed": true, "children": [...] }
```
### Баннер-предупреждение
@@ -233,7 +233,7 @@ allowed-tools: []
{ "input": "ДанныеКоличество", "path": "Объект.Данные.Количество", "on": ["OnChange"] },
{ "input": "ДанныеСумма", "path": "Объект.Данные.Сумма", "readOnly": true }
]},
{ "group": "horizontal", "name": "ГруппаКнопок", "children": [
{ "autoCmdBar": "ФормаКоманднаяПанель", "children": [
{ "button": "Загрузить", "command": "Загрузить", "title": "Загрузить из файла", "defaultButton": true },
{ "button": "Очистить", "command": "Очистить", "title": "Очистить таблицу" },
{ "button": "Закрыть", "stdCommand": "Close" }
+1 -1
View File
@@ -31,7 +31,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/form-remove/scripts/remove-form.ps1 -ObjectName "<ObjectName>" -FormName "<FormName>" [-SrcDir "<SrcDir>"]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/remove-form.ps1" -ObjectName "<ObjectName>" -FormName "<FormName>" [-SrcDir "<SrcDir>"]
```
## Что удаляется
@@ -1,4 +1,4 @@
# form-remove v1.1 — Remove form from 1C object
# form-remove v1.2 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -12,6 +12,8 @@ param(
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Проверки ---
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# remove-form v1.0 — Remove form from 1C object
# remove-form v1.1 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
+2 -2
View File
@@ -23,7 +23,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/form-validate/scripts/form-validate.ps1 -FormPath "Catalogs/Номенклатура/Forms/ФормаЭлемента"
powershell.exe -NoProfile -File .claude/skills/form-validate/scripts/form-validate.ps1 -FormPath "src/МояОбработка/Forms/Форма/Ext/Form.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-validate.ps1" -FormPath "Catalogs/Номенклатура/Forms/ФормаЭлемента"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-validate.ps1" -FormPath "src/МояОбработка/Forms/Форма/Ext/Form.xml"
```
@@ -1,7 +1,8 @@
# form-validate v1.4 — Validate 1C managed form
# form-validate v1.6 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$FormPath,
[switch]$Detailed,
@@ -365,13 +366,51 @@ if (-not $stopped) {
$dataPath = $dpNode.InnerText.Trim()
if (-not $dataPath) { 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 ($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
}
$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': 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)) {
Report-Error "[$tag] '$elName': DataPath='$dataPath' — attribute '$rootAttr' not found"
$pathErrors++
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-validate v1.4 — Validate 1C managed form
# form-validate v1.6 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -57,7 +57,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Validate 1C managed form", allow_abbrev=False)
parser.add_argument("-FormPath", required=True)
parser.add_argument("-FormPath", "-Path", required=True)
parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30)
args = parser.parse_args()
@@ -376,12 +376,44 @@ def main():
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
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}': DataPath='{data_path}' \u2014 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}': DataPath='{data_path}' \u2014 attribute '{root_attr}' not found")
path_errors += 1
+1 -1
View File
@@ -30,7 +30,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/help-add/scripts/add-help.ps1 -ObjectName "<ObjectName>" [-Lang "<Lang>"] [-SrcDir "<SrcDir>"]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/add-help.ps1" -ObjectName "<ObjectName>" [-Lang "<Lang>"] [-SrcDir "<SrcDir>"]
```
## Что делает скрипт
+3 -1
View File
@@ -1,4 +1,4 @@
# help-add v1.3 — Add built-in help to 1C object
# help-add v1.4 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -10,6 +10,8 @@ param(
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Detect format version ---
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# add-help v1.3 — Add built-in help to 1C object
# add-help v1.4 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: img-grid
description: Наложить пронумерованную сетку на изображение для определения пропорций колонок
description: Наложить пронумерованную сетку на изображение. Используй при анализе скриншота макета или печатной формы — измерить пропорции колонок перед генерацией табличного документа
argument-hint: <ImagePath> [-c COLS]
allowed-tools:
- Bash
@@ -29,7 +29,7 @@ allowed-tools:
## Команда
```bash
python .claude/skills/img-grid/scripts/overlay-grid.py "<ImagePath>" [-c 50] [-o "<OutputPath>"]
python "${CLAUDE_SKILL_DIR}/scripts/overlay-grid.py" "<ImagePath>" [-c 50] [-o "<OutputPath>"]
```
Требуется Python 3 с библиотекой Pillow (`pip install Pillow`).
+2 -2
View File
@@ -29,13 +29,13 @@ allowed-tools:
### Inline mode
```powershell
powershell.exe -NoProfile -File '.claude/skills/interface-edit/scripts/interface-edit.ps1' -CIPath '<path>' -Operation hide -Value '<cmd>'
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-edit.ps1" -CIPath '<path>' -Operation hide -Value '<cmd>'
```
### JSON mode
```powershell
powershell.exe -NoProfile -File '.claude/skills/interface-edit/scripts/interface-edit.ps1' -CIPath '<path>' -DefinitionFile '<json>'
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-edit.ps1" -CIPath '<path>' -DefinitionFile '<json>'
```
## Операции
@@ -1,7 +1,7 @@
# interface-edit v1.3 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$CIPath,
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
[string]$DefinitionFile,
[ValidateSet("hide","show","place","order","subsystem-order","group-order")]
[string]$Operation,
@@ -182,7 +182,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Edit 1C CommandInterface.xml", allow_abbrev=False)
parser.add_argument("-CIPath", required=True)
parser.add_argument("-CIPath", "-Path", required=True)
parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["hide", "show", "place", "order", "subsystem-order", "group-order"])
parser.add_argument("-Value", default=None)
@@ -504,7 +504,7 @@ def main():
if os.path.isfile(validate_script):
print()
print("--- Running interface-validate ---")
subprocess.run([sys.executable, validate_script, "-CIPath", resolved_path])
subprocess.run([sys.executable, validate_script, "-CIPath", "-Path", resolved_path])
# --- Summary ---
print()
+2 -2
View File
@@ -24,6 +24,6 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File ".claude/skills/interface-validate/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи"
powershell.exe -NoProfile -File ".claude/skills/interface-validate/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи/Ext/CommandInterface.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи/Ext/CommandInterface.xml"
```
@@ -1,7 +1,7 @@
# interface-validate v1.1 — Validate 1C CommandInterface.xml structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$CIPath,
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
[switch]$Detailed,
[int]$MaxErrors = 30,
[string]$OutFile
@@ -79,7 +79,7 @@ def main():
parser = argparse.ArgumentParser(
description='Validate 1C CommandInterface.xml structure', allow_abbrev=False
)
parser.add_argument('-CIPath', dest='CIPath', required=True)
parser.add_argument('-CIPath', '-Path', dest='CIPath', required=True)
parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: meta-compile
description: Создать объект метаданных 1С. Используй когда пользователь просит создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др.
description: Создать объект метаданных 1С. Используй когда нужно создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др.
argument-hint: <JsonPath> <OutputDir>
allowed-tools:
- Bash
@@ -23,7 +23,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-compile/scripts/meta-compile.ps1 -JsonPath "<json>" -OutputDir "<ConfigDir>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>"
```
| Параметр | Описание |
@@ -18,7 +18,7 @@
| `checkUnique` | `false` | CheckUnique |
| `defaultPresentation` | `AsDescription` | DefaultPresentation |
| `subordinationUse` | `ToItems` | SubordinationUse |
| `quickChoice` | `true` | QuickChoice |
| `quickChoice` | `false` | QuickChoice |
| `choiceMode` | `BothWays` | ChoiceMode |
| `owners` | `[]` | Owners |
| `attributes` | `[]` | → Attribute в ChildObjects |
@@ -1,4 +1,4 @@
# meta-compile v1.10 — Compile 1C metadata object from JSON
# meta-compile v1.12 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -502,6 +502,7 @@ function Parse-AttributeShorthand {
fillChecking = if ($val.fillChecking) { "$($val.fillChecking)" } else { "" }
indexing = if ($val.indexing) { "$($val.indexing)" } else { "" }
multiLine = if ($val.multiLine -eq $true) { $true } else { $false }
choiceHistoryOnInput = if ($val.choiceHistoryOnInput) { "$($val.choiceHistoryOnInput)" } else { "" }
}
}
@@ -822,7 +823,8 @@ function Emit-Attribute {
X "$indent`t`t<CreateOnInput>Auto</CreateOnInput>"
X "$indent`t`t<ChoiceForm/>"
X "$indent`t`t<LinkByType/>"
X "$indent`t`t<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>"
$chi = if ($parsed.choiceHistoryOnInput) { $parsed.choiceHistoryOnInput } else { "Auto" }
X "$indent`t`t<ChoiceHistoryOnInput>$chi</ChoiceHistoryOnInput>"
# Use — only for catalog top-level attributes
if ($context -eq "catalog") {
@@ -1129,7 +1131,7 @@ function Emit-CatalogProperties {
X "$i<Characteristics/>"
X "$i<PredefinedDataUpdate>Auto</PredefinedDataUpdate>"
X "$i<EditType>InDialog</EditType>"
$quickChoice = if ($def.quickChoice -eq $false) { "false" } else { "true" }
$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
$choiceMode = Get-EnumProp "ChoiceMode" "choiceMode" "BothWays"
X "$i<QuickChoice>$quickChoice</QuickChoice>"
X "$i<ChoiceMode>$choiceMode</ChoiceMode>"
@@ -1292,7 +1294,8 @@ function Emit-EnumProperties {
Emit-StandardAttributes $i "Enum"
X "$i<Characteristics/>"
X "$i<QuickChoice>true</QuickChoice>"
$quickChoice = if ($def.quickChoice -eq $false) { "false" } else { "true" }
X "$i<QuickChoice>$quickChoice</QuickChoice>"
X "$i<ChoiceMode>BothWays</ChoiceMode>"
X "$i<DefaultListForm/>"
X "$i<DefaultChoiceForm/>"
@@ -1663,7 +1666,8 @@ function Emit-ExchangePlanProperties {
X "$i<IncludeConfigurationExtensions>$includeExt</IncludeConfigurationExtensions>"
X "$i<BasedOn/>"
X "$i<QuickChoice>true</QuickChoice>"
$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
X "$i<QuickChoice>$quickChoice</QuickChoice>"
X "$i<ChoiceMode>BothWays</ChoiceMode>"
X "$i<InputByString>"
X "$i`t<xr:Field>ExchangePlan.$objName.StandardAttribute.Description</xr:Field>"
@@ -1764,7 +1768,8 @@ function Emit-ChartOfCharacteristicTypesProperties {
X "$i<Characteristics/>"
X "$i<PredefinedDataUpdate>Auto</PredefinedDataUpdate>"
X "$i<EditType>InDialog</EditType>"
X "$i<QuickChoice>true</QuickChoice>"
$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
X "$i<QuickChoice>$quickChoice</QuickChoice>"
X "$i<ChoiceMode>BothWays</ChoiceMode>"
X "$i<InputByString>"
X "$i`t<xr:Field>ChartOfCharacteristicTypes.$objName.StandardAttribute.Description</xr:Field>"
@@ -1905,7 +1910,8 @@ function Emit-ChartOfAccountsProperties {
X "$i<Characteristics/>"
X "$i<PredefinedDataUpdate>Auto</PredefinedDataUpdate>"
X "$i<QuickChoice>true</QuickChoice>"
$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
X "$i<QuickChoice>$quickChoice</QuickChoice>"
X "$i<ChoiceMode>BothWays</ChoiceMode>"
X "$i<InputByString>"
X "$i`t<xr:Field>ChartOfAccounts.$objName.StandardAttribute.Description</xr:Field>"
@@ -2021,7 +2027,8 @@ function Emit-ChartOfCalculationTypesProperties {
X "$i<Characteristics/>"
X "$i<PredefinedDataUpdate>Auto</PredefinedDataUpdate>"
X "$i<EditType>InDialog</EditType>"
X "$i<QuickChoice>true</QuickChoice>"
$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
X "$i<QuickChoice>$quickChoice</QuickChoice>"
X "$i<ChoiceMode>BothWays</ChoiceMode>"
X "$i<InputByString>"
X "$i`t<xr:Field>ChartOfCalculationTypes.$objName.StandardAttribute.Description</xr:Field>"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-compile v1.10 — Compile 1C metadata object from JSON
# meta-compile v1.12 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -465,6 +465,7 @@ def parse_attribute_shorthand(val):
'fillChecking': str(val['fillChecking']) if val.get('fillChecking') else '',
'indexing': str(val['indexing']) if val.get('indexing') else '',
'multiLine': True if val.get('multiLine') is True else False,
'choiceHistoryOnInput': str(val['choiceHistoryOnInput']) if val.get('choiceHistoryOnInput') else '',
}
def parse_enum_value_shorthand(val):
@@ -774,7 +775,8 @@ def emit_attribute(indent, parsed, context):
X(f'{indent}\t\t<CreateOnInput>Auto</CreateOnInput>')
X(f'{indent}\t\t<ChoiceForm/>')
X(f'{indent}\t\t<LinkByType/>')
X(f'{indent}\t\t<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>')
chi = parsed.get('choiceHistoryOnInput') or 'Auto'
X(f'{indent}\t\t<ChoiceHistoryOnInput>{chi}</ChoiceHistoryOnInput>')
if context == 'catalog':
X(f'{indent}\t\t<Use>ForItem</Use>')
if context not in ('processor', 'processor-tabular'):
@@ -1024,7 +1026,7 @@ def emit_catalog_properties(indent):
X(f'{i}<Characteristics/>')
X(f'{i}<PredefinedDataUpdate>Auto</PredefinedDataUpdate>')
X(f'{i}<EditType>InDialog</EditType>')
quick_choice = 'false' if defn.get('quickChoice') is False else 'true'
quick_choice = 'true' if defn.get('quickChoice') is True else 'false'
choice_mode = get_enum_prop('ChoiceMode', 'choiceMode', 'BothWays')
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>{choice_mode}</ChoiceMode>')
@@ -1157,7 +1159,8 @@ def emit_enum_properties(indent):
X(f'{i}<UseStandardCommands>false</UseStandardCommands>')
emit_standard_attributes(i, 'Enum')
X(f'{i}<Characteristics/>')
X(f'{i}<QuickChoice>true</QuickChoice>')
quick_choice = 'false' if defn.get('quickChoice') is False else 'true'
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
X(f'{i}<DefaultListForm/>')
X(f'{i}<DefaultChoiceForm/>')
@@ -1471,7 +1474,8 @@ def emit_exchange_plan_properties(indent):
X(f'{i}<DistributedInfoBase>{distributed}</DistributedInfoBase>')
X(f'{i}<IncludeConfigurationExtensions>{include_ext}</IncludeConfigurationExtensions>')
X(f'{i}<BasedOn/>')
X(f'{i}<QuickChoice>true</QuickChoice>')
quick_choice = 'true' if defn.get('quickChoice') is True else 'false'
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>ExchangePlan.{obj_name}.StandardAttribute.Description</xr:Field>')
@@ -1557,7 +1561,8 @@ def emit_chart_of_characteristic_types_properties(indent):
X(f'{i}<Characteristics/>')
X(f'{i}<PredefinedDataUpdate>Auto</PredefinedDataUpdate>')
X(f'{i}<EditType>InDialog</EditType>')
X(f'{i}<QuickChoice>true</QuickChoice>')
quick_choice = 'true' if defn.get('quickChoice') is True else 'false'
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>ChartOfCharacteristicTypes.{obj_name}.StandardAttribute.Description</xr:Field>')
@@ -1673,7 +1678,8 @@ def emit_chart_of_accounts_properties(indent):
X(f'{i}</StandardTabularSections>')
X(f'{i}<Characteristics/>')
X(f'{i}<PredefinedDataUpdate>Auto</PredefinedDataUpdate>')
X(f'{i}<QuickChoice>true</QuickChoice>')
quick_choice = 'true' if defn.get('quickChoice') is True else 'false'
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>ChartOfAccounts.{obj_name}.StandardAttribute.Description</xr:Field>')
@@ -1764,7 +1770,8 @@ def emit_chart_of_calculation_types_properties(indent):
X(f'{i}<Characteristics/>')
X(f'{i}<PredefinedDataUpdate>Auto</PredefinedDataUpdate>')
X(f'{i}<EditType>InDialog</EditType>')
X(f'{i}<QuickChoice>true</QuickChoice>')
quick_choice = 'true' if defn.get('quickChoice') is True else 'false'
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>ChartOfCalculationTypes.{obj_name}.StandardAttribute.Description</xr:Field>')
+2 -2
View File
@@ -18,13 +18,13 @@ allowed-tools:
### Inline mode (простые операции)
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-edit/scripts/meta-edit.ps1 -ObjectPath "<path>" -Operation <op> -Value "<val>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -ObjectPath "<path>" -Operation <op> -Value "<val>"
```
### JSON mode (сложные/комбинированные)
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-edit/scripts/meta-edit.ps1 -DefinitionFile "<json>" -ObjectPath "<path>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -DefinitionFile "<json>" -ObjectPath "<path>"
```
| Параметр | Описание |
@@ -4,6 +4,7 @@ param(
[string]$DefinitionFile,
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ObjectPath,
# Inline mode (alternative to DefinitionFile)
@@ -2121,7 +2121,7 @@ def main():
parser = argparse.ArgumentParser(description="Edit existing 1C metadata object XML", allow_abbrev=False)
parser.add_argument("-DefinitionFile", default=None, help="JSON definition file")
parser.add_argument("-ObjectPath", required=True, help="Path to object XML or directory")
parser.add_argument("-ObjectPath", "-Path", required=True, help="Path to object XML or directory")
parser.add_argument("-Operation", default=None, choices=valid_operations, help="Inline operation")
parser.add_argument("-Value", default=None, help="Inline value")
parser.add_argument("-NoValidate", action="store_true", help="Skip auto-validation")
@@ -2257,7 +2257,7 @@ def main():
print()
print("--- Running meta-validate ---")
python_exe = sys.executable
subprocess.run([python_exe, validate_script, "-ObjectPath", resolved_path])
subprocess.run([python_exe, validate_script, "-ObjectPath", "-Path", resolved_path])
else:
print()
print(f"[SKIP] meta-validate not found at: {validate_script}")
+1 -1
View File
@@ -23,7 +23,7 @@ allowed-tools:
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-info/scripts/meta-info.ps1 -ObjectPath "<путь>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-info.ps1" -ObjectPath "<путь>"
```
## Три режима
@@ -1,7 +1,7 @@
# meta-info v1.1 — Compact summary of 1C metadata object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][string]$ObjectPath,
[Parameter(Mandatory=$true)][Alias('Path')][string]$ObjectPath,
[ValidateSet("overview","brief","full")]
[string]$Mode = "overview",
[string]$Name,
@@ -13,7 +13,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-ObjectPath", required=True)
parser.add_argument("-ObjectPath", "-Path", required=True)
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview")
parser.add_argument("-Name", default="")
parser.add_argument("-Limit", type=int, default=150)
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: meta-remove
description: Удалить объект метаданных из конфигурации 1С. Используй когда пользователь просит удалить, убрать объект из конфигурации
description: Удалить объект метаданных из конфигурации 1С. Используй когда нужно удалить, убрать объект из конфигурации
argument-hint: <ConfigDir> -Object <Type.Name>
allowed-tools:
- Bash
@@ -32,7 +32,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-remove/scripts/meta-remove.ps1 -ConfigDir "<путь>" -Object "Catalog.Товары"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-remove.ps1" -ConfigDir "<путь>" -Object "Catalog.Товары"
```
## Поддерживаемые типы
+2 -2
View File
@@ -24,6 +24,6 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-validate/scripts/meta-validate.ps1 -ObjectPath "Catalogs/Номенклатура/Номенклатура.xml"
powershell.exe -NoProfile -File .claude/skills/meta-validate/scripts/meta-validate.ps1 -ObjectPath "Catalogs/Банки|Documents/Заказ"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-validate.ps1" -ObjectPath "Catalogs/Номенклатура/Номенклатура.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-validate.ps1" -ObjectPath "Catalogs/Банки|Documents/Заказ"
```
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ObjectPath,
[switch]$Detailed,
@@ -14,7 +14,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-ObjectPath", required=True)
parser.add_argument("-ObjectPath", "-Path", required=True)
parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30)
parser.add_argument("-OutFile", default="")
@@ -31,7 +31,7 @@ if len(path_list) > 1:
batch_ok = 0
batch_fail = 0
for single_path in path_list:
cmd = [sys.executable, __file__, "-ObjectPath", single_path, "-MaxErrors", str(max_errors)]
cmd = [sys.executable, __file__, "-ObjectPath", "-Path", single_path, "-MaxErrors", str(max_errors)]
if detailed:
cmd.append("-Detailed")
if out_file:
+1 -1
View File
@@ -29,7 +29,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/mxl-compile/scripts/mxl-compile.ps1 -JsonPath "<путь>.json" -OutputPath "<путь>/Template.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -JsonPath "<путь>.json" -OutputPath "<путь>/Template.xml"
```
## Рабочий процесс
+1 -1
View File
@@ -29,7 +29,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 -TemplatePath "<путь>/Template.xml" [-OutputPath "<путь>.json"]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-decompile.ps1" -TemplatePath "<путь>/Template.xml" [-OutputPath "<путь>.json"]
```
## Рабочий процесс
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$TemplatePath,
[string]$OutputPath
@@ -47,7 +47,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Decompile 1C spreadsheet to JSON", allow_abbrev=False)
parser.add_argument("-TemplatePath", required=True, help="Path to Template.xml")
parser.add_argument("-TemplatePath", "-Path", required=True, help="Path to Template.xml")
parser.add_argument("-OutputPath", default=None, help="Output JSON path (stdout if omitted)")
args = parser.parse_args()
+2 -2
View File
@@ -38,12 +38,12 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/mxl-info/scripts/mxl-info.ps1 -TemplatePath "<путь>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-info.ps1" -TemplatePath "<путь>"
```
Или по имени обработки/макета:
```powershell
powershell.exe -NoProfile -File .claude/skills/mxl-info/scripts/mxl-info.ps1 -ProcessorName "<Имя>" -TemplateName "<Макет>" [-SrcDir "<каталог>"]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-info.ps1" -ProcessorName "<Имя>" -TemplateName "<Макет>" [-SrcDir "<каталог>"]
```
Дополнительные флаги:
@@ -1,6 +1,7 @@
# mxl-info v1.0 — Analyze 1C spreadsheet structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Alias('Path')]
[string]$TemplatePath,
[string]$ProcessorName,
[string]$TemplateName,

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