Compare commits

...

196 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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 afdfc97fb1 fix(web-test): readSpreadsheet — поддержка text-only и отчётов с числовыми шапками
Рефакторинг buildSpreadsheetMapping на 3-уровневый алгоритм.
- Level 1: якорь по DCS-кодам (К1..Кn) — детерминированный для всех ФСД-отчётов, работает независимо от формата чисел (рубли/тыс/млн).
- Level 2: якорь по форматированным числам (пробел-группировка, запятая-десятичка, ведущий минус) вместо общей проверки — голые целые (коды счетов "50", "51") больше не принимаются за данные.
- Level 3: single-row header fallback для text-only данных и query-console.

Починено:
- ФСД-отчёты с числами в групповых шапках (ДДС по счетам 50/51/52/55/57) — был fallback raw rows, теперь структурированный вывод.
- query() из consoleЗапросов для text-only результатов — был data=[], теперь корректно парсит headers/data.

E2E проверено на titan: 4 отчёта (ДС, 45, 77, Ведомость) + 5 query-кейсов. Регрессий нет.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:38:47 +03:00
Nick Shirokov 82e70d2c30 feat(skd-compile,form-compile): Phase 3 — project-level presets
- skd-compile v1.12: scan-up from OutputPath for presets/skills/skd/skd-styles.json (PS1+PY)
- form-compile presets/README.md: full preset documentation (sections, keys, enums, example)
- docs/form-guide.md: --from-object mode + project-level presets sections
- skd-compile SKILL.md: updated styles search path description

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:08:22 +03:00
Nick Shirokov aedf6df674 fix(form-compile,meta-compile): PY ChartOfAccounts generators — list format + AccountingFlag name extraction
- form-compile.py: rewrite generate_chart_of_accounts_item_dsl and
  generate_chart_of_accounts_folder_dsl from dict-format to list-format
  (array of OrderedDict), matching PS1 canonical output
- meta-compile.py/ps1: extract flag['name'] from AccountingFlags and
  ExtDimensionAccountingFlags dicts instead of stringifying the whole object
- Update snapshots with clean flag names (Валютный/Количественный)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:55:33 +03:00
Nick Shirokov 0d31d17204 fix(form-compile): PY register list generators — array format for columns/elements
Fixed IR List and AccumReg List PY generators:
- columns: OrderedDict → list of OrderedDict (matching PS1 array format)
- table element: use 'table' key (not 'element'), 'tableAutofill' (not 'autoCommandBar'), 'None' (not 'none')
- elements: list (not OrderedDict wrapper)

PY tests: 10/12 (2 remaining CoA failures — PY CoA Item generator needs deeper rewrite from dict to list format)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:17:23 +03:00
Nick Shirokov 2206c4cf3e fix(form-compile): convert new generators from dict to array DSL format
Root cause: new generators (IR, AccumReg, CoA, CCOCT/EP wrappers) used
OrderedDict for elements/columns, but PS1 compiler expects array format.
ConvertTo-Json→ConvertFrom-Json wraps dict into single PSCustomObject,
not iterable array — so ChildItems were empty.

Converted all new generators to array format matching existing
Document/Catalog patterns: elements=@(), columns=@().

Also fixed CCOCT/EP wrapper inject logic to iterate array elements
instead of dict keys.

PS1: 12/12, PY: 8/12 (minor case/autofill differences in PY port).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:11:55 +03:00
Nick Shirokov 8a9f285da9 fix(form-compile): port bugfixes to Python — Number/Date in lists, UserVisible for Ref
Port PS1 bugfixes to Python:
- Document List: add Номер + Дата as first columns
- Hidden Ref: userVisible=false instead of visible=false (both Catalog and Document lists)
- Emitter: support <UserVisible><xr:Common>false</xr:Common></UserVisible>
- Add userVisible to KNOWN_KEYS

Note: new_field_element calls in Python were already correct (no Bug 1 equivalent).
Python snapshots updated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:55:56 +03:00
Nick Shirokov e5e3f199f2 fix(form-compile): empty forms, missing Number/Date in lists, UserVisible for Ref
Three bugs fixed in --from-object PS1 generators:

1. New-FieldElement called with wrong positional args in IR Record and CoA Item
   generators — hashtable passed as attrName instead of individual fields.
   Result: elements became "System.Collections.Hashtable" → compiler dropped them
   → empty forms. Fixed with named parameters.

2. Document List form missing Number/Date standard columns — only custom
   attributes were shown. Added Номер + Дата as first two columns.

3. Hidden Ref column used Visible=false (element completely hidden from
   "Customize form"). Changed to UserVisible=false so users can enable Ref
   and add sub-columns via dot notation. Matches ERP Контрагенты pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:34:45 +03:00
Nick Shirokov 3ee715939b test(form-compile): regression tests for new --from-object types
8 test cases covering InformationRegister (Record periodic/nonperiodic, List),
AccumulationRegister (List), ChartOfCharacteristicTypes (Item),
ExchangePlan (Item), ChartOfAccounts (Item, List).

All 12 tests pass on both PS1 and Python runtimes with form-validate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:21:46 +03:00
Nick Shirokov b8ab791456 feat(form-compile): add --from-object support for 5 new object types
Add InformationRegister (Record/List), AccumulationRegister (List),
ChartOfCharacteristicTypes (Item/Folder/List/Choice via Catalog delegation),
ExchangePlan (Item/List/Choice via Catalog delegation),
ChartOfAccounts (Item/Folder/List/Choice with AccountingFlags + ExtDimensionTypes).

Generalize extractAttrs → extractFields with tag parameter.
Add preset defaults and erp-standard.json keys for all new types.
Bump version to v1.6 in both PS1 and PY.

Also: form-add now supports AccumulationRegister (PS1+PY).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:21:30 +03:00
Nick Shirokov 97a2ef91d5 fix(form-compile): skip ValueStorage attributes in --from-object mode
ValueStorage is a non-displayable type that cannot be bound to form
elements. Filter it out in all generators: catalog item, catalog/document
list columns, document item (unclaimed + footer).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:02:31 +03:00
Nick Shirokov a41897a966 test(form-compile): regression tests for --from-object mode
4 snapshot tests: catalog item/list (Валюты-like) + document item/list
(АктВВР-like). Verified against platform 1C 8.3.24. Register
form-compile-from-object in verify-snapshots DEFAULT_SKILLS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:40:12 +03:00
Nick Shirokov 076bbcb9d5 feat(form-compile): add --from-object mode for auto form generation from metadata
Read 1C object XML (Document/Catalog), apply ERP preset, generate Form.xml
automatically. Supports Item/List/Choice/Folder purposes with auto-resolve
of object path and purpose from OutputPath convention. Also extends DSL
with DynamicList Settings, Table choiceMode/initialTreeView/enableDrag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:03:34 +03:00
Nick Shirokov 3c48451704 docs(meta-compile): add new Catalog props and multiline flag to specs
Update meta-dsl-spec.md and types-basic.md reference with:
limitLevelCount, levelCount, foldersOnTop, codeSeries,
subordinationUse, quickChoice, choiceMode, multiline flag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:00:04 +03:00
Nick Shirokov 801ceea2c0 feat(meta-compile): configurable Catalog props, owners, multiLine, fix reservedAttrNames
- Catalog: limitLevelCount, levelCount, foldersOnTop, subordinationUse,
  codeSeries, quickChoice, choiceMode now read from JSON (were hardcoded)
- Catalog owners: new `owners` array property with shorthand normalization
- Attribute MultiLine: configurable via `multiLine: true` or `| multiline` flag
- reservedAttrNames warning: now skipped for tabular/processor-tabular context
- 3 new enum validations: SubordinationUse, CodeSeries, ChoiceMode

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:54:50 +03:00
Nick Shirokov 63de8bd27c fix(meta-compile,meta-edit,meta-validate): strict enum validation + fix RequireCalculationTypes
Normalize-EnumValue now uses 4-step logic: alias→case-insensitive→
error (if propName known)→pass-through (if unknown). Previously step 3
silently passed invalid values through to XML, causing cryptic 1C
LoadConfigFromFiles errors.

Also fixed RequireCalculationTypes→OnActionPeriod (the former never
existed in 1C; verified against ERP/ACC dumps). Added NotUsed→DontUse
alias, synced meta-edit.ps1 aliases with meta-compile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:29:46 +03:00
Nick Shirokov d1e770c843 test(form-edit): declare Поле1 in preRun form-compile attributes
add-element and add-group-with-fields built their baseline form with an
InputField whose DataPath pointed to "Поле1", but "Поле1" was never
declared as a form attribute. runner.mjs snapshot diffing accepted the
output, but verify-snapshots caught the real XDTO error at load time:
"Неверный путь к данным: Поле1".

Add the missing attribute to both preRun form-compile inputs and
regenerate snapshots (the new attribute takes id=5, so form-edit's added
"Поле2" now lands at id=6).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:03:34 +03:00
Nick Shirokov e3069ceb36 fix(form-compile): throw on known invalid attribute types
KNOWN_INVALID_TYPES (FormDataStructure, FormDataCollection, FormDataTree,
etc.) was checked but only produced a Write-Warning/print warning — the
script still emitted the bad <v8:Type> into Form.xml, which XDTO later
rejected with a cryptic load-time error. Turn the warning into a hard
throw so misuse is caught at compile time with the correct hint.

Reveals two broken test cases that shipped invalid forms:
- form-compile/catalog-form: main attribute was FormDataStructure, fixed
  to CatalogObject.Товары (what ERP's reference catalog forms actually
  use with the cfg: prefix).
- form-info/overview: preRun form-compile used the same wrong type, fixed
  the same way; snapshot regenerated.

Bumps form-compile to v1.4 on both runtimes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:03:24 +03:00
Nick Shirokov 0b2c09f8d9 fix(verify-snapshots): support case fixture setup and workDir cwd
Two harness gaps that masked real issues and leaked stray files:

1. Case-level `setup: "fixture:<name>"` was ignored — runner.mjs handled
   it, verify-snapshots did not. skd-edit/add-drilldown silently failed
   with "File not found: Template.xml" because the fixture never reached
   workDir. Added Step 0 fixture copy mirroring runner.mjs behavior.

2. `skillConfig.cwd === "workDir"` was ignored — main skill always ran
   with cwd=REPO_ROOT. mxl-compile cases pass relative -OutputPath
   "Template.xml", which landed in the repo root on every run. Plumb cwd
   through execSkill and set mainCwd from skillConfig.cwd.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:03:11 +03:00
Nick Shirokov 0d5d3451ff fix(cf-edit): reject add-childObject when object file is missing
cf-edit add-childObject was a low-level XML-manipulation operation
with no file-existence validation — callers could register a reference
to any Type.Name in Configuration.xml's ChildObjects without the
underlying file existing on disk. Platform then refused to load:
"Файл объекта не существует".

The 4 failing tests (add-objects, remove-object, add-default-role,
set-default-roles) all used this operation with fake references in
either main input or preRun, and had no way to pass verify-snapshots
because the cf-init-ed config had no actual object files.

User observation: this is the tests being wrong, not the skill.
meta-compile/role-compile/subsystem-compile already auto-register
every new object in Configuration.xml as part of their normal flow
(meta-compile.ps1:2949-3068, role-compile.ps1:667-747,
subsystem-compile.ps1:430-506). Nobody should be calling cf-edit
add-childObject to create a new object — they should be calling the
profile skill. cf-edit add-childObject is only for rare recovery
scenarios: rolled-back Configuration.xml with intact object files,
re-import from DB dump that clobbered the root but left srcfiles.

Changes:

1. cf-edit.ps1/py: Do-AddChildObject now checks that the target file
   exists at {ConfigDir}/{PluralDir}/{Name}.xml before registering.
   On miss, exits 1 with a message that names the expected path and
   points the user at the right skill (/meta-compile, /role-compile,
   or /subsystem-compile depending on type). TYPE_TO_DIR mapping for
   all 44 metadata types covers irregular plurals (FilterCriteria,
   BusinessProcesses, ChartsOfAccounts, ChartsOfCharacteristicTypes,
   ChartsOfCalculationTypes).

2. Tests: 4 existing cases rewritten to build realistic fixtures via
   meta-compile/role-compile preRun (both skills auto-register, so
   the resulting Configuration.xml already references the preRun
   objects). add-objects now exercises the round-trip recovery
   scenario: meta-compile creates Catalog.Товары and Document.ПриходТоваров
   (auto-registered) → cf-edit remove-childObject un-registers both
   (files remain) → main run re-registers via add-childObject. This
   tests exactly the rollback-recovery use case the operation exists for.

3. New add-missing-errors case: negative test with expectError:
   "Object file not found". Verifies the new hard-error path.

4. verify-snapshots.mjs: added symmetric expectError handling (runner.mjs
   already had it at line 514). If caseData.expectError is set,
   expect skill to fail; check stderr substring match; skip db-load
   and mark passed. Without this, negative tests would go red in
   verify-snapshots even though runner.mjs accepts them.

5. SKILL.md / reference.md: documented the new constraint and the
   redirection to profile skills. Kept mention of legitimate use case
   (rollback recovery).

Bumped cf-edit.ps1/py v1.0→v1.1.

Verification:
- runner --filter cf-edit (PS1): 2/6 → 7/7 (6 positive + 1 negative)
- runner --filter cf-edit --runtime python: 7/7 (dual-port clean)
- verify-snapshots --skill cf-edit: 2/6 → 7/7

With this landed P3 from debug/snapshot-verify/NEXT-STEPS.md is closed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:18:05 +03:00
Nick Shirokov 7a8f437e77 test(subsystem-compile): cover bottom-up -Parent flow; hide children shortcut
The bottom-up flow (compile child with -Parent pointing at parent's
XML — skill creates the real child file AND registers it in parent's
ChildObjects) has been the documented canonical way to build nested
subsystems since forever. It's in SKILL.md Примеры:58 and implemented
in subsystem-compile.ps1:430-506. But zero test cases exercised it —
all 7 pre-existing cases used the top-down `children: [...]` shortcut
that aa93031 made honest with stubs.

Two problems with the status quo:

1. A model reading SKILL.md saw `"children": ["ДочерняяА", "ДочерняяБ"]`
   right in the main JSON-definition example and took it as the
   canonical way to create nested structure. It's a trap — the
   shortcut creates placeholder stubs with empty Synonym/Content that
   the model almost never actually wants. The natural flow (one
   subsystem-compile call per real subsystem) wasn't visible where
   the model looks first.

2. The canonical flow had no test safety net — nothing caught regressions
   in the register-in-parent code path (lines 430-506).

Fix, minimal surface:

- SKILL.md: remove `"children": [...]` from the JSON-definition example.
  Leave the `-Parent` example in the Примеры section (already there).
  The children field stays fully supported in the scripts (aa93031 stub
  behavior unchanged) for legacy JSON — just not advertised.

- New test case `nested-parent.json`: preRun compiles "Продажи" parent,
  main run compiles "Настройки" child with `-Parent Subsystems/Продажи.xml`.
  Verifies the real bottom-up flow: snapshot shows full child file with
  real Synonym/Explanation AND parent's `<ChildObjects>` updated to
  reference the child. verify-snapshots confirms platform accepts it.

- Runner plumbing: `_skill.json` gains `{ "flag": "-Parent", "from":
  "workPath", "field": "parent", "optional": true }`. Required extending
  both `tests/skills/runner.mjs` and `tests/skills/verify-snapshots.mjs`
  (they each have their own copy of buildArgs) to support `optional: true`
  on workPath mappings — otherwise existing cases without params.parent
  would get the flag pushed with an empty value.

Verification:
- runner --filter subsystem-compile (PS1): 8/8 (was 7/7 +1)
- runner --filter subsystem-compile --runtime python: 8/8 (dual-port clean)
- verify-snapshots --skill subsystem-compile: 8/8

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 19:15:07 +03:00
Nick Shirokov dc4ffa1fc8 fix(form-compile): interpolate \$script:formatVersion in <Form> header
Third victim of the d155086 single-quote regression that 037062c
missed. Both <Form> header emissions at lines 1130 and 1140 used
`X '...version="$($script:formatVersion)"...'` — single-quoted, so
the literal text `$($script:formatVersion)` landed in the output
XML instead of the detected version number.

The bug was masked for a week because:
1. form-compile runner tests weren't rerun against the broken script
   after d155086 (snapshots still showed the pre-regression
   `version="2.17"` hardcode)
2. verify-snapshots was already red on form-compile for other reasons
   (P2 XDTO errors in some cases), so nobody noticed the wholesale
   script breakage
3. The .py port uses an f-string and was never broken

Found while auditing whether 037062c was complete — the earlier grep
for `'...\$formatVersion...'` single-line patterns had missed this
because `$($script:formatVersion)` is a subexpression-in-string form
that wasn't in the grep pattern.

Fix: convert both X calls to double-quoted strings with backtick-
escaped inner quotes, matching the 037062c pattern for
role-compile/subsystem-compile. Same approach, same precedent.

Bumped form-compile.ps1 v1.2→v1.3.

Verification:
- runner --filter form-compile (PS1): 0/10 → 10/10
- runner --filter form-compile --runtime python: 10/10 (dual-port clean)
- verify-snapshots --skill form-compile: surfaced from fully-masked
  to 9/10 (only catalog-form still fails — real P2 XDTO issue, not
  \$formatVersion)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:59:26 +03:00
Nick Shirokov 6805b9da02 fix(subsystem-edit): write stub XML on add-child operation
Do-AddChild / do_add_child added `<Subsystem>Name</Subsystem>` to the
parent's `<ChildObjects>`, but never wrote the corresponding
`Subsystems/{Parent}/Subsystems/{Name}.xml` file. Same silent-drop
pattern that bit subsystem-compile (aa93031): platform used to swallow
the missing-file reference, `-StrictLog` now surfaces it as "Файл
объекта не существует" and fails add-child on load.

Both ports now mirror the subsystem-compile fix:
- Write-ChildSubsystemStub / write_child_subsystem_stub helpers
  duplicated from subsystem-compile (per memory rule "skills are
  autonomous, duplication acceptable")
- format_version read from loaded XmlDoc root (no need to walk up
  to Configuration.xml — we already have the parent XML in memory)
- Stub creation guarded by Test-Path / os.path.exists so a pre-existing
  real child file is never clobbered

Bumped subsystem-edit.ps1 v1.1→v1.2 and subsystem-edit.py v1.1→v1.2.

Verification:
- verify-snapshots --skill subsystem-edit: 3/4 → 4/4
- runner --filter subsystem-edit (PS1): 4/4
- runner --filter subsystem-edit --runtime python: 4/4 (dual-port drift clean)

With this landed P1 from debug/snapshot-verify/NEXT-STEPS.md is fully
closed: subsystem-compile 7/7, subsystem-edit 4/4, interface-edit 4/4,
role-compile 8/8, meta-compile 30/30.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:49:40 +03:00
Nick Shirokov aa93031a3f fix(subsystem-compile): write stub XML for declared children
When a subsystem definition includes `children: [...]`, the parent XML
was emitted with `<ChildObjects><Subsystem>Name</Subsystem></ChildObjects>`
refs, but the referenced `Subsystems/{Parent}/Subsystems/{Child}.xml`
files were never created. Before 96d1dea (-StrictLog) the platform
silently dropped the refs on load (exit 0), so verify-snapshots showed
these cases green. With the new strict log parsing, `full` and
`with-children` started failing on "Файл объекта не существует".

Both PS1 and PY ports now emit a minimal valid child subsystem stub
(full MetaDataObject, empty Synonym/Content/ChildObjects) via new
Write-ChildSubsystemStub / write_child_subsystem_stub helpers. Stub
creation is guarded by Test-Path / os.path.exists, so a subsequent
compile of the same child via -Parent does not get clobbered, and
re-running the parent compile is idempotent. Дубли в children[]
дедуплицируются через seen-set.

Also removed the "Что генерируется" section from SKILL.md — filesystem
layout is covered by the OutputDir param + [OK] stdout lines; the
section was noise for model consumers.

Bumped subsystem-compile.ps1 v1.4→v1.5 and subsystem-compile.py
v1.3→v1.5 (PY caught up with PS1 version pin).

Verification:
- verify-snapshots --skill subsystem-compile: 5/7 → 7/7
- runner --filter subsystem-compile (PS1): 7/7
- runner --filter subsystem-compile --runtime python: 7/7 (dual-port drift clean)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:38:03 +03:00
Nick Shirokov 037062c728 fix(role-compile,subsystem-compile): interpolate \$formatVersion in XML output
Regression introduced by d155086 (auto-detect XML format version):
both scripts emit their root MetaDataObject element via X '...' with
single-quoted strings, which PowerShell does NOT interpolate. As a
result the literal text \$formatVersion landed in the generated XML,
and every load failed with "Неизвестная версия формата \$formatVersion".

This was masked for a week because the broken call sites aren't
version-dependent by themselves — the platform exits with code 1 on
this error, but verify-snapshots hadn't been re-run cleanly since
the offending commit (we only did a scoped role-compile smoke test
that happened to pass for unrelated reasons).

Fixed by switching both single-quoted X '...' calls to double-quoted
X "..." with escaped inner quotes. meta-compile / form-compile /
epf-add-form / help-add / template-add / interface-edit already used
here-strings or \$script:formatVersion with double-quoted wrappers
and were unaffected.

Bumped role-compile.ps1 to v1.5 and subsystem-compile.ps1 to v1.4.
verify-snapshots --skill role-compile now 8/8 green. subsystem-compile
re-verification is pending other unrelated fixes (see NEXT-STEPS.md).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:01:00 +03:00
Nick Shirokov 9cbda1989a test(meta-compile/document-journal): preRun creates documents with referenced attributes
Journal column references require the referenced document attribute to
actually exist at load time. Previously the test DSL relied on the
verify-snapshots stub mechanism, which creates minimal Document stubs
without the specific attributes the column refs point to → load failed
with "Неизвестный объект метаданных - Document.ПриходнаяНакладная.Attribute.Склад".
This was listed as D5 in the 2026-04-05 FINDINGS log ("low priority,
complex to implement").

Now the test case declares preRun steps that create both documents
with the exact attributes its journal columns reference (Склад on one,
Контрагент on both). Column "Контрагент" gained explicit references
(was a shorthand string before) because the platform rejects journal
columns without at least one reference at load time.

Regenerated the snapshot (gained Documents/ subtree from preRun output).
verify-snapshots --skill meta-compile is now 30/30 green with -StrictLog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:00:34 +03:00
Nick Shirokov d34ddd41ff fix(meta-compile): strip FillFrom/FillValue/DataHistory for non-Info register attributes
Custom attributes on AccumulationRegister, AccountingRegister and
CalculationRegister do NOT support FillFromFillingValue, FillValue or
DataHistory — platform logs "Неверное свойство объекта метаданных" and
silently drops them. InformationRegister DOES support these properties
(verified against erp_8.3.24 dump for both variants).

Split the single "register" Emit-Attribute context into:
  - register-info  → emits the three properties (InformationRegister)
  - register-other → skips them (Accum/Acc/Calc)

Chart* context already handled by 3ba6072 remains as-is. Extended the
exclusion list in Emit-Attribute to cover register-other symmetrically
for FillFromFillingValue, FillValue and DataHistory.

Updated snapshots:
  - accounting-register: removed the 3 bad lines on Содержание attribute
  - accumulation-register/calculation-register: added test attributes
    to exercise the register-other path and regenerate snapshots cleanly

Closes the silent-rejection class #4 from upload/form-baseline/gotchas.md,
now caught by verify-snapshots -StrictLog on the E2E platform load.

Bumped meta-compile.ps1 + .py to v1.8.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:00:12 +03:00
Nick Shirokov 96d1dea552 fix(db-load-xml): surface silent platform rejections + opt-in -StrictLog
Platform writes load-time property/type/enum rejections ("Неверное
свойство объекта метаданных", "Неизвестное имя типа" и т.п.) into the
/Out log but still exits with code 0, silently dropping the offending
metadata. db-load-xml now parses the log for these patterns and prints
a yellow "[warning] N rejection(s)" block to stdout so users (and the
model) can see them immediately.

Exit code still mirrors the platform by default — we don't second-guess
its verdict. With the new -StrictLog switch, rejection patterns are
elevated to exit code 1, which is the mode verify-snapshots.mjs uses
for honest E2E verdicts. All three db-load-xml call sites in the
verifier (main config, CFE base, CFE extension) now pass -StrictLog.

Found while investigating upload/form-baseline/gotchas.md #4 where AR
attribute emission was wrong but verify-snapshots showed green because
the old exit-code-only check missed the silent drops.

Bumped db-load-xml.ps1 + .py to v1.3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:59:50 +03:00
Nick Shirokov 83b289de32 feat(skd): canonical @autoDates pattern + new params operations + use preservation
Fix-pack from skills-improvements-v4 feedback addressing 6 issues found
during real-world ФСД report development.

skd-compile (v1.10 → v1.11):
- @autoDates: emit canonical БСП pattern for ДатаНачала/ДатаОкончания —
  with title, useRestriction=true, value 0001-01-01T00:00:00, expression.
  Removed availableAsField=false so БСП creates two separate Start/End
  fields in the quick settings panel (was rendering as a single picker).
- StandardPeriod value: always emit v8:startDate/v8:endDate to match how
  1C Designer saves the schema (avoids spurious diff on first re-save).
- parameter shorthand: support [Title] syntax mirroring add-field.

skd-edit (v1.9 → v1.10):
- modify-filter / modify-dataParameter: preserve <use> when @off/@on not
  explicitly set (was silently stripping <use>false</use>). Tristate
  parser: None=don't touch, False=@off, True=@on.
- modify-parameter: support [Title] for setting/replacing <title>.
- rename-parameter: new operation "OldName => NewName" — atomically
  renames parameter, updates &Name references in expressions of other
  parameters (full identifier match only), and dcscor:parameter entries
  in dataParameters of all variants. Query text is not touched.
- reorder-parameters: new operation "Name1, Name2, ..." — partial list,
  named params go first in given order, rest preserve original order.
- StandardPeriod value: same v8:startDate/v8:endDate fix as compile.

Tests: 4 new test cases (rename-parameter, reorder-parameters,
modify-parameter-title, modify-dataParameter-preserves-use).
48/48 passing on both PowerShell and Python runtimes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:22:40 +03:00
Nick Shirokov 384e68cab4 fix(skd-compile): object-form structure, OrGroup string items, useRestriction alias
- Default structure item type to 'group' when omitted; accept groupFields as alias for groupBy
- Parse string shorthand items inside OrGroup/AndGroup/NotGroup filter recursion
- Accept useRestriction key (object form { field: true }) alongside restrict array
- Deduplicate SelectedItemAuto in skd-edit add-selection
- Update SKILL.md with object structure and useRestriction docs
- Add test cases for all 4 fixes

skd-compile v1.9→v1.10, skd-edit v1.8→v1.9

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:54:38 +03:00
Nick Shirokov 46e065adb9 feat(skd-edit): add-drilldown operation for connecting DrillDown to DCS template resources
Adds DetailsAreaTemplateParameter + Расшифровка appearance binding
to all named templates for each specified resource. Comma-separated
value list, idempotent, nesting-aware template scan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:36:50 +03:00
Nick Shirokov fdfe4ac2f4 fix(skd-edit): parse #restriction flags in add-calculated-field shorthand
Previously #noFilter/#noOrder/#noGroup flags were included verbatim in
<expression> instead of generating <useRestriction>. Now parsed and
handled identically to add-field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 19:34:55 +03:00
Nick Shirokov afacaa5ade fix(web-test): readSpreadsheet header detection for DCS reports with account codes
- Strict isNumericVal check excludes account codes like "68/78" from being
  treated as data values (require pure digits+spaces+commas)
- Require >=2 numeric cells to identify data rows (fallback to >=1)
- Detect DCS column code rows (К1..Кn) and always prefix with group/superRow
- 3-level header support: superRow values used as prefix when group is empty
- superRow excluded from title/meta section
- Fuzzy column matching in clickElement for short codes ("К6" → "84 / К6")
- Replace newlines with spaces in cell text (innerText instead of textContent)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 19:25:37 +03:00
Nick Shirokov 73242ee60e fix(skd-compile): identity totalField shorthand treated as aggregation function
When totalField shorthand right-hand side is not a known aggregate function
(e.g. "Проверка: Проверка"), emit expression as-is instead of wrapping it
as Проверка(Проверка). Known aggregates (Сумма, Количество, etc.) still wrap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:33:36 +03:00
Nick Shirokov e2924c4ae0 fix(skd-compile): remove spurious vMerge flag from source cells in template DSL
The vertical merge flag (ОбъединятьПоВертикали) was incorrectly placed on
both the source cell (with content) and continuation cells ("|"). 1C only
expects it on continuation cells. Removed startsVMerge logic from both
PS1 and PY scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:49:56 +03:00
Nick Shirokov 123fc41b06 fix(web-test): selection form search order and type dialog fast path
pickFromSelectionForm: swap steps 2↔3 — try Alt+F advanced search
before search input to avoid overlay blocking row clicks.
pickFromTypeDialog: scan visible rows first, fall back to Ctrl+F
only for large virtual lists. Reduces 3s hardcoded wait to ~0.2s
for common case. scanGridRows: add isGroup flag via gridListH check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:43:44 +03:00
Nick Shirokov 338faa253d feat(web-test): multi-row grid support in readTable — split merged headers
readTable now handles multi-row grids (e.g. accounting journal entries)
where a single column header spans multiple data sub-rows:
- "Субконто Дт" with 3 data cells → "Субконто Дт 1", "Субконто Дт 2", "Субконто Дт 3"
- Stacked headers (2+ at same X) matched by Y-order (e.g. "Счет Дт" / "Подразделение Дт")
- getFormState tables[].columns also expanded for consistency
- Flat/simple grids unaffected (no multi-row detection triggers)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:41:58 +03:00
Nick Shirokov d3520a8945 fix(skd-edit): modify-parameter availableValue parsing and formatting bugs
Fix 3 bugs in modify-parameter: (1) first availableValue rendered as raw
text when combined with other kv pairs in same batch entry, (2) presentation
values with spaces truncated by \S+ regex, (3) denyIncompleteValues/use
inserted without line breaks. Root cause: if/else on rest.startsWith
missed availableValue when preceded by other keys. Also fix namespace-aware
element lookup using LocalName/local_name instead of SelectSingleNode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 21:34:04 +03:00
Nick Shirokov e731bde7f0 feat(skd-compile): horizontal cell merge ">" in template DSL
Add ">" cell syntax for horizontal merge (ОбъединятьПоГоризонтали),
analogous to "|" for vertical merge. Enables two-level headers with
colspan in DCS templates. Also fix PY decimal formatting (30.0 → 30).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 20:41:47 +03:00
Nick Shirokov 321e426f98 docs(skd): update dsl-spec and guide for new features, fix py compat
- skd-dsl-spec: availableValues/denyIncompleteValues, Folder in selection, DesignTimeValue/OrGroup in filters, Format as LocalStringType
- skd-guide: mention new CA types, Folder, availableValues
- Fix Python 3.13: inline regex flags, element truth-testing, OrGroup desc, dict structure wrap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:53:04 +03:00
Nick Shirokov 54c04cfe76 fix(skd): Python 3.13 compatibility fixes
- skd-edit.py: fix (?i) inline regex flag → re.IGNORECASE (Python 3.13 error)
- skd-edit.py: fix "if not sv" on XML element → "sv is None" (FutureWarning)
- skd-edit.py: fix OrGroup filter crash in output description (list vs dict)
- skd-compile.py: wrap dict structure in list before iteration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:48:35 +03:00
Nick Shirokov 9727635e5d test(skd): add snapshot tests for new features
- skd-edit: conditionalAppearance with DesignTimeValue/OrGroup/Format
- skd-edit: modify-parameter with use/denyIncompleteValues/availableValue
- skd-edit: set-structure @name= + add-selection Folder() @group=
- skd-compile: availableValues/denyIncompleteValues + Folder in selection
- Fix xsi namespace in @group= XPath query

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:42:12 +03:00
Nick Shirokov 6404016afb feat(skd-edit): add-selection @group= targets named StructureItemGroup
- add-selection supports @group=Name to add selection items to a named grouping instead of variant level
- Finds StructureItemGroup by dcsset:name, falls back to variant level if not found
- Document @group= in SKILL.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:35:07 +03:00
Nick Shirokov 17afd807d2 docs(skd): update SKILL.md for new features
- skd-edit: document modify-parameter, Folder() in selection, @name= in structure, OrGroup/DesignTimeValue/Format in conditionalAppearance
- skd-compile: document availableValues/denyIncompleteValues, Folder in selection, OrGroup, DesignTimeValue, Format as LocalStringType

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:32:14 +03:00
Nick Shirokov 9bfc431a6a feat(skd): @name= in set-structure, Folder in selection
- skd-edit: set-structure supports @name= for naming groupings (e.g. "Поле @name=ДанныеОтчета > details")
- skd-edit: add-selection supports Folder(Title: field1, field2) syntax for SelectedItemFolder
- skd-compile: selection supports {"folder": "Title", "items": [...]} for SelectedItemFolder
- Both generate lwsTitle, nested SelectedItemField items, and placement=Auto

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:23:18 +03:00
Nick Shirokov d755c41233 feat(skd): modify-parameter operation, availableValues/denyIncompleteValues support
- skd-edit: new modify-parameter operation — set use, denyIncompleteValues, add availableValue entries to existing parameters
- skd-compile: availableValues array and denyIncompleteValues in parameter JSON DSL
- Auto-detect DesignTimeValue type for reference values in availableValue entries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:19:32 +03:00
Nick Shirokov 87e636f644 feat(skd): DesignTimeValue in filters, Format as LocalStringType, OrGroup in conditionalAppearance
- Auto-detect DesignTimeValue type for enum/catalog/chart-of-accounts references in filter values (both skd-edit and skd-compile)
- Treat Формат appearance parameter as v8:LocalStringType (alongside Текст/Заголовок)
- Support OrGroup in conditionalAppearance filters via " or " syntax in skd-edit shorthand
- Bump skd-edit v1.5, skd-compile v1.6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:15:44 +03:00
Nick Shirokov ae1dcaac07 feat(web-test): detect SpreadsheetDocument state bar (stateText)
Extract info bar messages from .stateWindowSupportSurface elements
into errors.stateText — covers missing parameters, "report not
generated", "settings changed", and "generating..." states.
readSpreadsheet() now includes the state message in its error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:27:24 +03:00
Nick Shirokov d155086444 feat(skills): auto-detect XML format version from Configuration.xml
When working with existing configs dumped from newer platforms (8.3.27+),
XML files use version="2.20" instead of "2.17". Skills now detect the
version from the nearest Configuration.xml walking up the directory tree,
falling back to "2.17" if not found. This prevents format version mismatch
errors during LoadConfigFromFiles.

Updated skills (11): meta-compile, form-compile, form-add, template-add,
cfe-borrow, epf-add-form, help-add, role-compile, subsystem-compile,
interface-edit. Also fixed form-validate to accept version 2.20.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:53 +03:00
Nick Shirokov 940eafb8e4 fix(skd-edit): patch-query with empty replacement (delete substring)
.strip()/.Trim() in batch-splitting was stripping the trailing space
of the " => " separator, making " => " (delete) unrecognizable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:02:22 +03:00
Nick Shirokov e4dcef8c90 fix(skd-compile): DesignTimeValue, useRestriction for hidden, named structure groups
- fix: auto-detect DesignTimeValue for ПланСчетов/Справочник/Перечисление/Документ values (#9)
- fix: hidden params auto-set useRestriction=true alongside availableAsField=false (#11)
- feat: named groups in structure shorthand "ИмяГруппы[Поле] > details" (#10)
- version bump: skd-compile v1.5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:44:14 +03:00
Nick Shirokov 08688f5cab docs(skd): update specs for hidden, valueListAllowed, drilldown, groupHeaderTemplate
- skd-dsl-spec: @valueList, @hidden, field alias, dataParameters auto, drilldown, groupName/GroupHeader
- skd-guide: new parameter flags, dataParameters auto, groupName, drilldown
- 1c-dcs-spec: valueListAllowed element, DetailsAreaTemplateParameter, groupHeaderTemplate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:36:04 +03:00
Nick Shirokov 1bc5e8f07a feat(skd-compile): hidden, valueListAllowed, drilldown, groupHeaderTemplate, dataPath fix
- fix: calculatedField dataPath fallback from "field" key (#5)
- fix: groupHeaderTemplate vs groupTemplate, groupName support (#7)
- feat: @valueList / valueListAllowed for parameters (#4)
- feat: @hidden / hidden params + "dataParameters": "auto" (#1)
- feat: drilldown in template params — DetailsAreaTemplateParameter + appearance (#2+#3)
- fix(template-add): improved error message with path hint (#6)
- docs: SKILL.md updated with new keys and examples
- version bump: skd-compile v1.4, template-add v1.2

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:17:33 +03:00
Nick Shirokov 358830c65b feat(meta-validate): Check 10 — warn on empty registers and broken RegisterRecords refs
Add two new validations found via platform snapshot verification:
- Registers without any Dimensions/Resources/Attributes → platform rejects
- Document.RegisterRecords referencing non-existent register objects

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:27:18 +03:00
Nick Shirokov 3ba6072660 fix(meta-compile): strip FillFromFillingValue/FillValue/DataHistory for Chart* attributes
ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
attributes don't support FillFromFillingValue, FillValue, DataHistory
properties — platform rejects them with "Неверное свойство объекта
метаданных". Add "chart" context to Emit-Attribute to skip these.

Found via platform snapshot verification (Finding A1).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:11:47 +03:00
Nick Shirokov 20adf4f463 fix(tests): correct ExternalDataProcessorObject→DataProcessorObject in config-context DSLs
Fix test DSLs that used ExternalDataProcessorObject (EPF type) for
DataProcessors inside configurations. Also fix: chart-of-accounts
(remove maxExtDimensionCount without ПВХТ), calculation-register
(remove actionPeriod without infrastructure), document-multiple-tabparts
(remove registerRecords referencing non-existent register),
role-compile/explicit-rights (add dimensions to empty InformationRegister).

Regenerated all affected snapshots.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:10:49 +03:00
Nick Shirokov d5aacc9e60 fix(form-validate): context-aware Check 12 — ExternalDataProcessorObject is error in config
Detect config vs EPF context by walking up from FormPath looking for
Configuration.xml. ExternalDataProcessorObject/ExternalReportObject are
valid in EPF/ERF but cause XDTO exception in configuration context.

- EPF forms: no warning (ExternalDataProcessorObject is correct)
- Config forms: ERROR with hint to use DataProcessorObject/ReportObject
- Fix test DSLs: compiled-form, table-form used wrong External* type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:01:24 +03:00
Nick Shirokov 3bd69baae6 fix(form-validate): add ExternalDataProcessorObject/ExternalReportObject to valid cfg prefixes
Check 12 was flagging cfg:ExternalDataProcessorObject.X as "unrecognized cfg
prefix", but this is a valid XDTO type for external data processor (EPF) forms.
Found via snapshot verification against epf-add-form snapshots.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 19:56:47 +03:00
Nick Shirokov b0fdc32053 feat(tests): verify-snapshots v0.3 — CFE support, preRun ref scanning, full coverage
- Add two-stage CFE pipeline: load base config → load extension
- Scan preRun inputs for type refs (fixes D3: meta-edit/add-ts-attribute)
- Support args-only cases (cf-init, form-add, epf-init, template-add, etc.)
- Add InformationRegister stubs with dimension (fixes D6)
- Results: 134/145 verified (16 new CFE cases all pass)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 19:43:42 +03:00
Nick Shirokov 731a652cae feat(tests): add platform verification script for skill snapshots
Runs each test case through the full pipeline: cf-init → stubs → preRun →
skill script → cf-edit → db-create → LoadConfigFromFiles → UpdateDBCfg.
Handles typed-input, args-only, standalone (SKD/MXL), and EPF skills.

Results: 118/145 pass, findings documented in debug/snapshot-verify/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 19:35:59 +03:00
Nick Shirokov dd88f78969 fix(form-compile,form-validate): warn on invalid XDTO types, add Check 12
form-compile now warns when model uses runtime types like
FormDataStructure that don't exist in XML schema. Expanded cfg:
regex to cover all 25 known prefixes.

form-validate adds Check 12 — validates all <v8:Type> values:
ERROR for known-invalid types, WARN for unrecognized bare types,
pass-through for unknown namespaced types (future-proof).

Updated SKILL.md with full type reference and invalid type warning.
Updated docs/1c-form-spec.md with missing type groups.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:07:07 +03:00
Nick Shirokov ff068202e3 fix(tests): update remaining snapshots with plural→singular content refs
Snapshots for subsystem-info and interface-validate still had
Catalogs.Товары from before normalization was added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:48:27 +03:00
Nick Shirokov 91798e3838 feat(interface-edit): normalize command name type prefix (plural/Russian to singular)
All operations (hide, show, place, order) now auto-normalize
the first segment of command names — e.g. Catalogs.X → Catalog.X,
Справочник.X → Catalog.X.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:46:01 +03:00
Nick Shirokov 9620c3846a fix(subsystem): normalize content refs — plural/Russian to singular English
subsystem-compile and subsystem-edit now auto-normalize content type
prefixes (Catalogs→Catalog, Справочник→Catalog, Справочники→Catalog).
subsystem-validate detects plural forms as errors in check #6.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:39:17 +03:00
Nick Shirokov ffc34904c5 fix(web-test): adaptive header detection threshold for narrow spreadsheets
Hardcoded threshold of 3 non-empty cells prevented header detection in
spreadsheets with 1-2 columns (e.g. query console results). Use
Math.min(3, maxCol + 1) so narrow tables can still be parsed structurally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 19:19:24 +03:00
Nick Shirokov b008c820f9 fix(web-test): stop group header carry-forward leaking into unrelated columns
When a spreadsheet has 3 header levels (group → detail → codes), the
carry-forward logic for merged group headers would bleed into columns
belonging to different top-level groups. Detect a "super-row" above the
group row and reset carry-forward when a new top-level header starts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 18:51:10 +03:00
Nick Shirokov e5697d6f5c fix(web-test): reliable arrow-key scroll for off-screen spreadsheet cells
Rewrites scrollSpreadsheetToCell with fixes for multiple issues discovered
during E2E testing:

- Use Playwright boundingBox (page-level coords) instead of frame-internal
  getBoundingClientRect for visibility checks — frame's clientWidth is wider
  than the actual visible iframe area clipped by parent elements
- Use iframe element's boundingBox to determine visible region — cells behind
  the section panel (x < iframeBox.x) were incorrectly considered "visible"
  and focus clicks hit the section panel instead of the spreadsheet
- Use div[y]+div[x] attribute selectors instead of div.RxCy CSS classes —
  the RxCy class numbering differs from y/x attribute values
- Accept cellLoc parameter from caller instead of re-searching — avoids
  selector mismatch and handles cells missing from some rows
- Native click through mxlCurrBody overlay (page.mouse.click) for focus —
  frame.locator().click() bypasses overlay causing header/data desync,
  page.mouse.click() + frameEl.focus() doesn't transfer keyboard focus
- Pick rightmost/leftmost fully-visible cell for focus based on scroll
  direction — each arrow press immediately triggers platform scroll

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 18:17:04 +03:00
Nick Shirokov 29ee294de6 fix(web-test): arrow-key scroll for off-screen spreadsheet cells (WIP)
Scroll via arrow keys with native platform behavior. Works for
moderate scroll (few columns off-screen). Known limitations:
- Far off-screen columns may timeout
- Re-clicking between direction changes can break scroll context
- Edge cells (first/last column) may not fully scroll into view

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 16:30:18 +03:00
Nick Shirokov d72cbacfd6 fix(web-test): scroll spreadsheet cell into view before clicking
Cells outside the visible iframe area couldn't be clicked because
boundingBox() returned null. Now scrollIntoView() is called first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 14:45:31 +03:00
Nick Shirokov c8a7ba4683 docs(web-test): add getPage() usage example for keyboard shortcuts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:13:10 +03:00
Nick Shirokov ea8b28280d feat(web-test): add SpreadsheetDocument cell clicking to clickElement
Extend clickElement to support clicking cells in rendered reports
(SpreadsheetDocument). First argument accepts { row, column } object
where coordinates match readSpreadsheet() output. Text fallback also
searches spreadsheet iframes when element not found in main DOM.

Refactor readSpreadsheet internals into reusable helpers:
scanSpreadsheetCells, buildSpreadsheetMapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:03:05 +03:00
Nick Shirokov eebc2a0679 feat(skd-edit): add patch-query operation for substring replacement in queries
Addresses user feedback: set-query is all-or-nothing, and editing XML
directly is fragile due to escaping. patch-query allows targeted string
replacement via "old => new" shorthand, with batch mode support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:46:01 +03:00
Nick Shirokov 974e8ff5e4 fix(meta-compile): create Ext/ lazily and add modules for Constant/Enum
Empty Ext/ directories for Constants, Enums, and DocumentJournals caused
platform to wipe all extension modules during LoadConfigFromFiles.
Now Ext/ is only created when files will be placed in it, and
Constant gets ManagerModule + ValueManagerModule, Enum gets ManagerModule.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 14:55:14 +03:00
Nick Shirokov 47c2e5d48f fix(web-test): detect textarea forms and normalize Windows paths
Simple EPF forms with textarea fields were invisible to form detection
(formCount: 0) and misclassified as modal error dialogs. Also, backslash
paths in exec scripts caused "Invalid Unicode escape sequence" JS parse errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:31:49 +03:00
Nick Shirokov e56a932ee2 docs(switch): document --link __dirname limitation
Node.js resolves __dirname through junctions to the real target
path, not the junction location. This causes Node.js-based skills
(e.g. web-test) to write output files to the skills repo directory
instead of the project directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:01:10 +03:00
Nick Shirokov 69f5e0b7ae docs(switch): mark --link as experimental
Junction/symlink install mode may cause intermittent MSYS bash
crashes on Windows (add_item / exit code 5). Demote from
recommended to experimental; recommend copy-based install instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:15:26 +03:00
Nick Shirokov 84462e3dd9 feat(web-test): highlight command groups on function panel
highlight() now supports command group headers (eAccentColor labels)
on the 1C function panel. Matches group name, collects header +
commands below it, draws multi-element bounding box overlay.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:27:50 +03:00
Nick Shirokov d574320849 fix(web-publish): auto-detect Apache download URL from apachelounge.com
Replace hardcoded Apache zip URL with dynamic parsing of the Apache
Lounge download page. Finds the latest Win64 build automatically,
so the script won't break when new versions are published.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:38:42 +03:00
Nick Shirokov 90ff1d53b6 feat(switch): add --link flag for junction/symlink install
Instead of copying skill folders, --link creates directory junctions
(Windows) or symlinks (Linux/Mac) so updates propagate automatically
via git pull. Only supported for claude-code platform (other platforms
require path rewriting in SKILL.md). Also adds safe_rmtree to prevent
shutil.rmtree from following junctions and deleting source files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:24:10 +03:00
Nick Shirokov 09bc0d00b8 fix(web-test): use target.y coordinates to find expand icon row
The expand/collapse code re-searched for the target row by first-cell
text, which was ambiguous when parent and child rows share the same
prefix (e.g. "БУ"). This caused expand to hit the wrong (already-
expanded) row and skip. Use target.y from the initial findClickTarget
instead — matches the exact row.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:17:40 +03:00
Nick Shirokov 66c6dc7aa1 fix(web-test): swap gridListH/V in isExpanded — hierarchy expand was inverted
gridListH = collapsed (▶), gridListV = expanded (▼). The old code had it
backwards, so `expand: true` on a collapsed group was a no-op.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:48:40 +03:00
Nick Shirokov 1d89b3ec69 refactor(skills): trim SKILL.md — remove file trees, output previews, usage advice
Remove redundant sections from 11 SKILL.md files:
- "Что генерируется" file trees (meta-compile, cf-init, cfe-init, epf-init, erf-init, form-add)
- Output previews (meta-remove, form-edit, cfe-diff, role-info)
- "Когда использовать" (meta-remove, form-edit, mxl-info)
- Internal details (meta-remove ref table, mxl-info column sets, overflow protection)
-276 lines total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 21:36:03 +03:00
Nick Shirokov 43d1d09ee8 fix(skills): SKILL.md gaps — missing params, clarify docs, fix example bug
- interface-edit: expand SKILL.md from stub to full docs, remove redundant reference.md
- cf-edit: restore reference.md link with descriptive text
- cfe-borrow: clarify BorrowMainAttribute semantics (omit/Form/All)
- epf-bsp-add-command: fix example bug (Модификатор on non-print command)
- mxl-validate: keep only universal -TemplatePath in docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 21:17:46 +03:00
Nick Shirokov 404e6c6851 refactor(*-validate): trim SKILL.md — remove check tables, exit codes, clarify Detailed
All 11 validate skills: remove internal check tables and exit code lines
that provide no value to the model-user. Update Detailed param description
to be clearer. -221 lines, ~1550 tokens saved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:55:05 +03:00
Nick Shirokov 009022d04b fix(web-test): close DLB hint popup before paste fallback in fillReferenceField
When DLB dropdown shows only a hint ("Введите строку для поиска...") without
.eddText items, the code fell through without closing the popup. This left
editDropDown covering the input field, causing Playwright to wait up to 30s
for actionability on the next page.click(). Now we Escape the hint popup
when eddState.visible=true but items are empty (34s → 5s on cold cache).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:12:41 +03:00
Nick Shirokov 70e684d189 feat(skill-tests): add form-add + form-compile steps to platform integration tests
Now that ExtendedPresentation and InterfaceCompatibilityMode bugs are fixed,
platform integration tests can include full form generation:
- platform-config: form-add + form-compile for Catalog and Document forms
- platform-epf: epf-add-form + form-compile with elements/attributes/commands
- platform-cfe: form-add + form-compile for borrowed Catalog form

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:17:25 +03:00
Nick Shirokov 72bad1aaaa fix(form-add,cf-init,cfe-init): ExtendedPresentation + InterfaceCompatibilityMode
- form-add v1.2: ExtendedPresentation only for DataProcessor/Report/External* forms
  (Catalogs, Documents, Registers etc. don't have this property — platform rejects it)
- cf-init v1.1: InterfaceCompatibilityMode Taxi → TaxiEnableVersion8_2
  (matches all real configs: acc 8.3.20/24/27, erp 8.3.24)
- cfe-init v1.1: read InterfaceCompatibilityMode from -ConfigPath base config
  (analogous to existing CompatibilityMode auto-detection)
- Remove workaround in platform-cfe integration test (cf-edit modify-property)
- Update 162 snapshot Configuration.xml + 7 form metadata snapshots

Tests: 301/301 passed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:08:42 +03:00
Nick Shirokov 29a5cbae4c feat(skill-tests): platform integration tests via .v8-project.json
Runner reads v8path from .v8-project.json, skips platform tests if
1cv8.exe unavailable. Placeholders: {v8path}, {v8exe}, {dbPath}, etc.

- platform-config: cf-init → meta-compile → db-create → load → update
- platform-epf: epf-init → epf-build → db-create → epf-dump (roundtrip)
- platform-cfe: config + extension → db-create → load both → update both

All 6 integration tests green (3 file-only + 3 platform).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:59:44 +03:00
Nick Shirokov be1bbb2d26 feat(skill-tests): negative cases for platform-dependent skills
Add expectError test cases for db-create, db-load-xml, db-dump-xml,
db-dump-cf, db-load-cf, db-update, db-run, epf-build, epf-dump.
Tests parameter validation (missing args, bad file paths, partial
mode without required params). Total: 301 cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:40:26 +03:00
Nick Shirokov 5281fd54f2 feat: meta-edit v1.5 — normalize enum property values
Same alias dictionary + case-insensitive matching as meta-compile v1.4.
Applied at: fillChecking/indexing in attribute parsing, and scalar
property change in modify-attribute/modify-property operations.
Both PS1 and PY versions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:30:04 +03:00
Nick Shirokov f697ba3ff3 feat: meta-compile v1.4 — normalize enum property values
Add alias dictionary + case-insensitive matching for 23 system enum
properties (RegisterType, WriteMode, Periodicity, etc.). Accepts common
model mistakes like "Balances"→"Balance", "RecordSubordinate"→
"RecorderSubordinate", Russian synonyms, and wrong-case values.

Both PS1 and PY versions updated with identical dictionaries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:30:04 +03:00
Nick Shirokov 0778cc89ee feat: post-run validation + integration tests for skill pipeline
- runner.mjs v0.4: --with-validation flag runs validators on real output
- postValidate config in 20 _skill.json files (maps skill → validator)
- validatePath in ~100 positive test cases
- skipValidation for 5 cross-reference cases (isolated workspace limitation)
- Integration tests: build-config (19 steps), build-epf (6), build-cfe (4)
- base-config cache from build-config for downstream tests
- Fix chart-of-calculation-types test data (DependenceOnCalculationTypes)
- 285/285 unit + 3/3 integration, all green with validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:30:03 +03:00
Nick Shirokov 28b2765f68 fix: accept XML-style synonyms in interface-edit and skd-compile DSL
interface-edit v1.1: place/order operations accept value as object
(not just JSON string) from DefinitionFile — no more JSON-in-JSON.

skd-compile v1.3: dataSetLinks accept both DSL names (sourceExpr,
destExpr, source, dest) and XML names (sourceExpression,
destinationExpression, sourceDataSet, destinationDataSet).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 14:11:02 +03:00
Nick Shirokov 0d116863ec feat: role-compile OutputDir accepts config root (like meta-compile)
- OutputDir now accepts config root dir — creates Roles/ subdirectory
- Back-compat: if OutputDir ends with "Roles", uses it as-is
- Configuration.xml lookup adjusted accordingly
- Updated SKILL.md, PS1, PY scripts (v1.3)
- Updated test cases and snapshots for new Roles/ path

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:51:24 +03:00
Nick Shirokov bc6bc01047 docs: update python-porting-guide with etree pitfalls from test session
- Fix save_xml_bom example: full declaration replace + lowercase encoding
- Add etree vs XmlDocument serialization differences table
- Add pitfalls: hashtable ordering, (?i) regex, missing property access

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:00:37 +03:00
Nick Shirokov 972cd5061d fix: resolve remaining 19 Python test failures — 285/285 on both runtimes
Script logic fixes (PY mirroring PS1):
- skd-compile: fix (?i) regex flag placement for Python 3.11+
- mxl-compile: handle list-of-lists row format (PS1 silently ignores)
- subsystem-compile: add "objects" → "content" synonym alias
- role-compile: add "rights" → "objects" synonym alias
- meta-compile: sort HTTP/Web service method/operation iteration
- form-edit: insert ChildItems after Events/AutoCommandBar (not at end)
- mxl-compile: sort colWidthMap iteration in both PS1 and PY for
  deterministic format indices

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 20:30:45 +03:00
Nick Shirokov 4565808b77 fix: Python XML compat — declaration quotes + runner normalization (112→266/285)
Scripts (production fix): fix XML declaration in 14 save_xml_bom scripts
- version='1.0' → version="1.0" (single→double quotes)
- encoding='UTF-8' → encoding="utf-8" (match PS1 XmlWriter output)
- Add trailing newline to etree.tostring output

Runner (test normalization, Python-only):
- normalizeXmlContent() applied only when --runtime python
- Handles etree serialization quirks: xmlns stripping, self-closing
  space, inter-tag whitespace, empty elements, &#13; entities
- PS1 tests remain strict — no normalization applied

19 remaining failures are real logic bugs in Python scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 19:05:49 +03:00
Nick Shirokov 250978c2fd fix: resolve FINDINGS — synonyms, path resolution, exit codes
Skill fixes (all ps1+py, version bumped to v1.1):
- role-compile: accept "rights" as synonym for "objects"
- subsystem-compile: accept "objects" as synonym for "content"
- form-add: resolve directory path to .xml (like meta-validate)
- form-info: resolve directory path to Ext/Form.xml (like form-validate)
- mxl-compile: support absolute OutputPath (IsPathRooted check)
- meta-remove: exit 1 when object not found; v1.1
- cfe-patch-method: accept plural type names (Catalogs → Catalog)

Test fixes:
- basic-role.json: use canonical "objects" key
- basic.json (subsystem): use canonical "content" key
- catalog-form.json: fix outdated DSL format
- New synonym test cases for role-compile and subsystem-compile
- error-not-found.json: expect error (exit 1)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:25:52 +03:00
Nick Shirokov 4551525718 feat: runner v0.3 — parallel execution, skip snapshots for external
Add parallel test execution with worker pool (default: CPU count).
New --concurrency N option (--concurrency 1 for sequential).
Pre-warm shared fixtures before parallel run.
Skip snapshot update/compare for external (read-only) workspaces —
prevents accidental copying of large config dumps.
Fix normalizeUuids for cf-info/cf-validate (false→true).

Result: 283 tests, 329s→84s (4x speedup).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:47:22 +03:00
Nick Shirokov 9422c66df4 feat: runner v0.2 — external setup, skip support, +24 real-data cases
Add `external:<path>` setup type for read-only access to real config
dumps without copying. Tests gracefully skip (○) when path unavailable.

Add 12 meta-compile cases for previously uncovered types (AccountingRegister,
CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes,
ChartOfCalculationTypes, BusinessProcess, Task, ExchangePlan,
DocumentJournal, EventSubscription, HTTPService, WebService).

Add 18 external cases for info/validate skills against real ACC 8.3.24
config dump (meta, form, skd, role, subsystem, cf).

Total: 283 tests, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:02:00 +03:00
Nick Shirokov a1b3fdd4e2 feat: deepen skill test coverage — 52 → 247 cases across all 43 skills
Add 195 new test cases covering examples from SKILL.md, edge cases,
and parameter combinations. Create _skill.json for form-edit, skd-edit,
subsystem-edit. Add fixtures for negative validate cases. Fix
normalizeUuids in meta-validate/meta-info configs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:38:06 +03:00
Nick Shirokov d6d44b8b35 feat: add interface-*, cfe-* tests (batches 8+9) — all skills covered
- interface-edit, interface-validate: 2 cases
- cfe-init, cfe-validate, cfe-borrow, cfe-patch-method, cfe-diff: 5 cases
- runner: switch params in preRun (true = no value)
- Findings: interface-edit JSON-in-JSON, cfe-patch-method type naming

52 tests across 40 skills, all passing (~48s).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 14:09:00 +03:00
Nick Shirokov b480fa0b49 feat: add form-*, skd-*, misc tests (batches 1, 2, 10)
New skills covered:
- form-add, form-compile, form-validate, form-info (batch 1)
- skd-compile, skd-validate, skd-info (batch 2)
- help-add, template-add, template-remove, meta-remove (batch 10)

Findings: form-add/form-info path resolution inconsistency,
meta-remove self-reference requires -Force.

45 tests across 33 skills, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 14:00:34 +03:00
Nick Shirokov 44a84f8ce7 feat: add role-*, subsystem-* tests (batches 4+5)
- role-compile, role-validate, role-info: 3 cases
- subsystem-compile, subsystem-validate, subsystem-info: 3 cases
- 34 tests across 22 skills, all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:52:38 +03:00
Nick Shirokov dcacecff7f feat: add cf-edit/validate/info, epf-init/validate/add-form, erf-init tests (batches 6+7)
7 new skills covered. All 28 tests across 16 skills passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:48:11 +03:00
Nick Shirokov 8b38f8f78d feat: add mxl-* tests (batch 3), support cwd in skill config and preRun
- mxl-compile, mxl-validate, mxl-info, mxl-decompile: 4 cases
- runner: cwd option in _skill.json and preRun steps for skills
  that resolve OutputPath relative to current directory
- Finding: mxl-compile only accepts relative OutputPath

21 tests across 9 skills, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:44:37 +03:00
Nick Shirokov 74b3f76a32 refactor: move broken fixtures into skill directory, remove global fixtures/
fixture: paths now resolve relative to skill's cases/ dir, not global.
Each validate skill keeps its broken fixtures locally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:31:34 +03:00
Nick Shirokov 671be7c6b5 docs: update README with all features — params, preRun, args_extra, workPath, verbose, failure workflow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:24:39 +03:00
Nick Shirokov 4a697db47a feat: show case id in failure output for easy navigation
Compact mode shows "cases/meta-compile/catalog-basic" next to failed
test name — model can open the file, rerun, or update snapshot.
Verbose mode shows id for all cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:21:25 +03:00
Nick Shirokov 34f582ddef feat: add total time to summary line
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:17:22 +03:00
Nick Shirokov eec626eb6f feat: compact output by default, --verbose for full tree
Default shows one line per skill: "✓ meta-compile  6/6 (3.3s)"
Failed tests expanded with details automatically.
--verbose/-v shows full tree with every case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:15:00 +03:00
Nick Shirokov 312d058412 feat: add meta-validate, meta-info tests and broken fixtures
- meta-validate: 3 cases (valid catalog, bad root element, file not found)
- meta-info: 2 cases (catalog overview with stdoutContains, not found error)
- fixtures/broken/catalog-bad-root for negative validate tests
- All 5 skill archetypes now covered: compile, init, edit, validate, info

17 tests, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:08:53 +03:00
Nick Shirokov 95776a4248 feat: add meta-edit tests, support preRun and workPath mapping
- meta-edit: 3 cases (add-attribute, add-tabpart, error-no-definition)
- runner: preRun steps for creating prerequisite objects before test
- runner: workPath arg mapping (workDir + case field) for path-based skills

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:05:17 +03:00
Nick Shirokov 347722ef0d feat: add cf-init test cases, support args_extra and params priority
- cf-init: 3 cases (basic, with-vendor, error-already-exists)
- runner: args_extra for optional CLI params per case
- runner: params.field takes priority over caseData.field in case.<field> mapping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:02:06 +03:00
Nick Shirokov 9f6793abae refactor: move snapshots into snapshots/ subdirectory
Reduces clutter when a skill has many test cases — all .json cases
are visible at top level, snapshots tucked away in one folder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:52:20 +03:00
Nick Shirokov 0ddb675502 feat: add skill regression test runner with meta-compile pilot
Snapshot-based test runner (tests/skills/runner.mjs) for verifying
skill script output. Zero dependencies, runs on any machine with
Node.js — no 1C platform needed for daily regression.

Pilot: meta-compile with 6 cases (4 positive with snapshots, 2 negative).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:48:51 +03:00
Nick Shirokov d2dfcfd160 fix(web-test): normalize \u00a0 in getFormState, add tooltip for icon-only buttons
getFormState now replaces non-breaking spaces with regular spaces in all
button names, field labels, checkbox/radio labels. Icon-only buttons
(pressCommand without text) expose tooltip from parent .framePress title
attribute. clickElement fuzzy match includes tooltip as lowest-priority
candidate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:27:07 +03:00
2016 changed files with 147447 additions and 7202 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/"
}
+11 -9
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'
```
## Операции
@@ -32,27 +32,29 @@ powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -Conf
| Операция | Формат Value | Описание |
|----------|-------------|----------|
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
| `add-childObject` | `Type.Name` (batch `;;`) | Добавить объект в ChildObjects |
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
| `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` (начальная страница) |
Подробнее: `reference.md` в каталоге навыка.
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
## Примеры
```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 -9
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,10 +35,7 @@
Формат: `Type.Name` — XML-тип и имя объекта через точку.
При добавлении объект вставляется в каноническую позицию:
1. Находит последний элемент того же типа → вставляет после
2. Если тип отсутствует → находит последний элемент предшествующего типа → вставляет после
3. Внутри одного типа — алфавитный порядок
**Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов.
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
@@ -48,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
@@ -58,6 +148,3 @@ Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"
]
```
## Авто-валидация
После сохранения автоматически запускается `cf-validate` (если не указан `-NoValidate`).
+356 -10
View File
@@ -1,9 +1,9 @@
# cf-edit v1.0 — 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
@@ -27,6 +27,7 @@ if (Test-Path $ConfigPath -PathType Container) {
}
if (-not (Test-Path $ConfigPath)) { Write-Error "File not found: $ConfigPath"; exit 1 }
$resolvedPath = (Resolve-Path $ConfigPath).Path
$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
# --- Load XML with PreserveWhitespace ---
$script:xmlDoc = New-Object System.Xml.XmlDocument
@@ -87,6 +88,22 @@ $script:typeOrder = @(
"BusinessProcess","Task","IntegrationService"
)
# --- Type → on-disk directory name (plural) ---
$script:typeToDir = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
"Constant"="Constants"; "CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents"
"DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences"; "DocumentJournal"="DocumentJournals"; "Enum"="Enums"
"Report"="Reports"; "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"; "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"; "IntegrationService"="IntegrationServices"
}
# --- XML manipulation helpers (from subsystem-edit pattern) ---
function Get-ChildIndent($container) {
foreach ($child in $container.ChildNodes) {
@@ -247,6 +264,29 @@ function Do-AddChildObject([string]$batchVal) {
exit 1
}
# Check that the referenced object actually exists on disk.
# cf-edit add-childObject is a low-level operation for rare scenarios
# (e.g. restoring a rolled-back Configuration.xml when object files are intact).
# For creating NEW objects, meta-compile/role-compile/subsystem-compile already
# auto-register in Configuration.xml — calling cf-edit add-childObject there is
# unnecessary and error-prone.
$typeDir = $script:typeToDir[$typeName]
$objFile = Join-Path (Join-Path $script:configDir $typeDir) "$objNameVal.xml"
if (-not (Test-Path $objFile)) {
$hintSkill = switch ($typeName) {
"Subsystem" { "subsystem-compile" }
"Role" { "role-compile" }
default { "meta-compile" }
}
Write-Error @"
Object file not found: $typeDir/$objNameVal.xml
cf-edit add-childObject only references objects that already exist on disk.
To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
/$hintSkill with {"type":"$typeName","name":"$objNameVal"}
"@
exit 1
}
# Dedup check
$existing = $false
foreach ($child in $script:childObjsEl.ChildNodes) {
@@ -404,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
@@ -468,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 }
}
}
+317 -11
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-edit v1.0 — 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
@@ -32,6 +33,22 @@ TYPE_ORDER = [
"BusinessProcess", "Task", "IntegrationService",
]
# Type → on-disk directory name (plural)
TYPE_TO_DIR = {
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
"Constant": "Constants", "CommonForm": "CommonForms", "Catalog": "Catalogs", "Document": "Documents",
"DocumentNumerator": "DocumentNumerators", "Sequence": "Sequences", "DocumentJournal": "DocumentJournals", "Enum": "Enums",
"Report": "Reports", "DataProcessor": "DataProcessors", "InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes", "ChartOfAccounts": "ChartsOfAccounts", "AccountingRegister": "AccountingRegisters",
"ChartOfCalculationTypes": "ChartsOfCalculationTypes", "CalculationRegister": "CalculationRegisters",
"BusinessProcess": "BusinessProcesses", "Task": "Tasks", "IntegrationService": "IntegrationServices",
}
ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"]
SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCatalogAddress"]
REF_PROPS = ["DefaultLanguage"]
@@ -131,7 +148,9 @@ def parse_batch_value(val):
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'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)
@@ -141,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()
@@ -169,6 +188,7 @@ def main():
print(f"File not found: {config_path}", file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(config_path)
config_dir = os.path.dirname(resolved_path)
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
@@ -283,6 +303,25 @@ def main():
sys.exit(1)
type_idx = TYPE_ORDER.index(type_name)
# Check that the referenced object actually exists on disk.
# cf-edit add-childObject is a low-level operation for rare scenarios
# (e.g. restoring a rolled-back Configuration.xml when object files are intact).
# For creating NEW objects, meta-compile/role-compile/subsystem-compile already
# auto-register in Configuration.xml — calling cf-edit add-childObject there is
# unnecessary and error-prone.
type_dir = TYPE_TO_DIR.get(type_name)
obj_file = os.path.join(config_dir, type_dir, f"{obj_name_val}.xml")
if not os.path.exists(obj_file):
hint_skill = {"Subsystem": "subsystem-compile", "Role": "role-compile"}.get(type_name, "meta-compile")
print(
f"Object file not found: {type_dir}/{obj_name_val}.xml\n"
f"cf-edit add-childObject only references objects that already exist on disk.\n"
f"To create a new {type_name}, use {hint_skill} (auto-registers in Configuration.xml):\n"
f' /{hint_skill} with {{"type":"{type_name}","name":"{obj_name_val}"}}',
file=sys.stderr
)
sys.exit(1)
# Dedup
exists = False
for child in child_objs_el:
@@ -455,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:
@@ -475,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)
@@ -500,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 -10
View File
@@ -24,16 +24,7 @@ allowed-tools:
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
```powershell
powershell.exe -NoProfile -File .claude/skills/cf-init/scripts/cf-init.ps1 -Name "МояКонфигурация"
```
## Что создаётся
```
<OutputDir>/
├── Configuration.xml # Корневой файл — все свойства
└── Languages/
└── Русский.xml # Язык по умолчанию
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация"
```
## Примеры
+36 -2
View File
@@ -1,4 +1,4 @@
# cf-init v1.0 — 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)]
@@ -160,7 +160,7 @@ $cfgXml = @"
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
<ModalityUseMode>DontUse</ModalityUseMode>
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
<DefaultConstantsForm/>
@@ -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"
+32 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-init v1.0 — 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
@@ -155,7 +155,7 @@ def main():
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
\t\t\t<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
\t\t\t<DefaultConstantsForm/>
@@ -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()
+3 -18
View File
@@ -17,28 +17,13 @@ allowed-tools:
| Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------|
| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки |
| Detailed | нет | — | Показывать [OK] для каждой проверки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда
```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 | XML well-formedness, MetaDataObject/Configuration, version 2.17/2.20 | ERROR |
| 2 | InternalInfo: 7 ContainedObject, валидные ClassId, уникальность | ERROR |
| 3 | Properties: Name непустой, Synonym, DefaultLanguage, DefaultRunMode | ERROR/WARN |
| 4 | Properties: enum-значения (11 свойств) | ERROR |
| 5 | ChildObjects: валидные имена типов (44 типа), нет дубликатов, порядок типов | ERROR/WARN |
| 6 | DefaultLanguage ссылается на существующий Language в ChildObjects | ERROR |
| 7 | Файлы языков Languages/<name>.xml существуют | WARN |
| 8 | Каталоги объектов из ChildObjects существуют (spot-check) | WARN |
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
@@ -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)
+2 -2
View File
@@ -31,7 +31,7 @@ allowed-tools:
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
| `Object` | Что заимствовать (обязат.), batch через `;;` |
| `BorrowMainAttribute` | Используй при добавлении нового реквизита на заимствованную форму. `Form` (по умолч.) — реквизиты с формы, `All` — все реквизиты объекта |
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
## Формат -Object
@@ -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,4 +1,4 @@
# cfe-borrow v1.2 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$ExtensionPath,
@@ -316,6 +316,25 @@ function Expand-SelfClosingElement($container, $parentIndent) {
}
}
# --- 7b. 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"
}
$script:formatVersion = Detect-FormatVersion $extDir
# --- 8. Namespaces declaration for object XML ---
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
@@ -466,7 +485,7 @@ function Borrow-Form {
$newFormUuid = [guid]::NewGuid().ToString()
$formMetaSb = New-Object System.Text.StringBuilder
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"2.17`">") | Out-Null
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
$formMetaSb.AppendLine("`t<Form uuid=`"${newFormUuid}`">") | Out-Null
$formMetaSb.AppendLine("`t`t<InternalInfo/>") | Out-Null
$formMetaSb.AppendLine("`t`t<Properties>") | Out-Null
@@ -498,7 +517,7 @@ function Borrow-Form {
$srcFormEl = $srcFormDoc.DocumentElement
$formVersion = $srcFormEl.GetAttribute("version")
if (-not $formVersion) { $formVersion = "2.17" }
if (-not $formVersion) { $formVersion = $script:formatVersion }
# Find direct children: form properties, AutoCommandBar, ChildItems
$srcAutoCmd = $null
@@ -1529,7 +1548,7 @@ function Build-BorrowedObjectXml {
$sb = New-Object System.Text.StringBuilder
$sb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
$sb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"2.17`">") | Out-Null
$sb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
$sb.AppendLine("`t<${typeName} uuid=`"${newUuid}`">") | Out-Null
# InternalInfo
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-borrow v1.2 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -254,6 +254,22 @@ XMLNS_DECL = (
)
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 get_child_indent(container):
if container.text and "\n" in container.text:
after_nl = container.text.rsplit("\n", 1)[-1]
@@ -305,7 +321,9 @@ def expand_self_closing(container, parent_indent):
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'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)
@@ -363,6 +381,8 @@ def main():
cfg_resolved = os.path.abspath(cfg_path)
cfg_dir = os.path.dirname(cfg_resolved)
format_version = detect_format_version(ext_dir)
# --- 2. Load extension Configuration.xml ---
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(ext_resolved, xml_parser)
@@ -499,7 +519,7 @@ def main():
lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append(f'<MetaDataObject {XMLNS_DECL} version="2.17">')
lines.append(f'<MetaDataObject {XMLNS_DECL} version="{format_version}">')
lines.append(f'\t<{type_name} uuid="{new_uuid_val}">')
lines.append(internal_info_xml)
lines.append("\t\t<Properties>")
@@ -1084,7 +1104,7 @@ def main():
new_form_uuid = new_guid()
form_meta_lines = [
'<?xml version="1.0" encoding="UTF-8"?>',
f'<MetaDataObject {XMLNS_DECL} version="2.17">',
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
f'\t<Form uuid="{new_form_uuid}">',
'\t\t<InternalInfo/>',
'\t\t<Properties>',
@@ -1111,7 +1131,7 @@ def main():
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
src_form_el = src_form_tree.getroot()
form_version = src_form_el.get("version", "2.17")
form_version = src_form_el.get("version", format_version)
src_auto_cmd = None
form_props = []
+1 -14
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 — обзор расширения
@@ -32,19 +32,6 @@ powershell.exe -NoProfile -File .claude/skills/cfe-diff/scripts/cfe-diff.ps1 -Ex
- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы
- `[OWN]` — собственный: количество реквизитов, ТЧ, форм
Пример вывода:
```
[BORROWED] Catalog.Валюты
&ИзменениеИКонтроль("РеквизитыРедактируемыеВГрупповойОбработке") — line 4 in ...
&Перед("ЗагрузитьКурсыВалют") — line 13 in ...
ChildObjects: 1 own attrs, 1 own TS, 3 own forms
Form.ФормаЭлемента (borrowed):
Event:OnCreateAtServer [After] -> Расш1_ПриСозданииПосле
Command:Подбор [Before] -> Расш1_ПодборПеред
Form.Расш1_МояФорма (own)
[OWN] Catalog.Расш5_Справочник1
```
Для каждой формы заимствованного объекта показывается:
- `(borrowed)` / `(own)` — заимствованная или собственная форма
- callType-события формы и элементов
+1 -12
View File
@@ -44,18 +44,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-init/scripts/cfe-init.ps1 -Name "МоёРасширение"
```
## Что создаётся
```
<OutputDir>/
├── Configuration.xml # Свойства расширения
├── Languages/
│ └── Русский.xml # Язык (заимствованный)
└── Roles/ # Если не -NoRole
└── <Prefix>ОсновнаяРоль.xml
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение"
```
## Примеры
+12 -3
View File
@@ -1,4 +1,4 @@
# cfe-init v1.0 — Create 1C configuration extension scaffold (CFE)
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -69,7 +69,7 @@ if ($ConfigPath) {
Write-Host "[WARN] Base config language not found: $baseLangFile"
}
# 3b. Read CompatibilityMode from base config
# 3b. Read CompatibilityMode and InterfaceCompatibilityMode from base config
$baseCfgDoc = New-Object System.Xml.XmlDocument
$baseCfgDoc.PreserveWhitespace = $false
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
@@ -82,7 +82,16 @@ if ($ConfigPath) {
} else {
Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode"
}
$ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs)
if ($ifcNode -and $ifcNode.InnerText) {
$InterfaceCompatibilityMode = $ifcNode.InnerText.Trim()
Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode"
} else {
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode"
}
} else {
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading."
}
@@ -184,7 +193,7 @@ $cfgXml = @"
<Copyright/>
<VendorInformationAddress/>
<ConfigurationInformationAddress/>
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
<InterfaceCompatibilityMode>$InterfaceCompatibilityMode</InterfaceCompatibilityMode>
</Properties>
<ChildObjects>$childObjectsXml</ChildObjects>
</Configuration>
+12 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-init v1.0 — Create 1C configuration extension scaffold (CFE)
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration extension."""
import sys, os, argparse, uuid
@@ -84,7 +84,7 @@ def main():
else:
print(f"[WARN] Base config language not found: {base_lang_file}")
# Read CompatibilityMode from base config
# Read CompatibilityMode and InterfaceCompatibilityMode from base config
try:
base_cfg_tree = ET.parse(os.path.abspath(config_path))
base_cfg_root = base_cfg_tree.getroot()
@@ -95,9 +95,18 @@ def main():
print(f"[INFO] Base config CompatibilityMode: {compat}")
else:
print(f"[WARN] CompatibilityMode not found in base config, using default: {compat}")
ifc_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:InterfaceCompatibilityMode', ns)
if ifc_node is not None and ifc_node.text:
ifc_mode = ifc_node.text.strip()
print(f"[INFO] Base config InterfaceCompatibilityMode: {ifc_mode}")
else:
ifc_mode = "TaxiEnableVersion8_2"
print(f"[WARN] InterfaceCompatibilityMode not found in base config, using default: {ifc_mode}")
except Exception:
print(f"[WARN] Could not parse base config, using default CompatibilityMode: {compat}")
ifc_mode = "TaxiEnableVersion8_2"
else:
ifc_mode = "TaxiEnableVersion8_2"
print("[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading.")
# --- Generate UUIDs ---
@@ -173,7 +182,7 @@ def main():
\t\t\t<Copyright/>
\t\t\t<VendorInformationAddress/>
\t\t\t<ConfigurationInformationAddress/>
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
\t\t\t<InterfaceCompatibilityMode>{ifc_mode}</InterfaceCompatibilityMode>
\t\t</Properties>
\t\t<ChildObjects>{child_objects_xml}</ChildObjects>
\t</Configuration>
+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
```
## Примеры
@@ -1,201 +1,209 @@
# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$ExtensionPath,
[Parameter(Mandatory)]
[string]$ModulePath,
[Parameter(Mandatory)]
[string]$MethodName,
[Parameter(Mandatory)]
[ValidateSet("Before","After","ModificationAndControl")]
[string]$InterceptorType,
[string]$Context = "НаСервере",
[switch]$IsFunction
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve extension path ---
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
}
if (Test-Path $ExtensionPath -PathType Leaf) {
$ExtensionPath = Split-Path $ExtensionPath -Parent
}
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
if (-not (Test-Path $cfgFile)) {
Write-Error "Configuration.xml not found in: $ExtensionPath"
exit 1
}
# --- Read NamePrefix from Configuration.xml ---
$cfgDoc = New-Object System.Xml.XmlDocument
$cfgDoc.PreserveWhitespace = $false
$cfgDoc.Load($cfgFile)
$cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgDoc.NameTable)
$cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$propsNode = $cfgDoc.SelectSingleNode("//md:Configuration/md:Properties", $cfgNs)
$prefixNode = if ($propsNode) { $propsNode.SelectSingleNode("md:NamePrefix", $cfgNs) } else { $null }
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "Расш_" }
# --- Map ModulePath to file path ---
# ModulePath formats:
# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl
# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl
# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl
# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl
# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl
$typeDirMap = @{
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
"CommonModule"="CommonModules"; "Report"="Reports"; "DataProcessor"="DataProcessors"
"ExchangePlan"="ExchangePlans"; "ChartOfAccounts"="ChartsOfAccounts"
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
"AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters"
}
$parts = $ModulePath.Split(".")
if ($parts.Count -lt 2) {
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module or CommonModule.Name"
exit 1
}
$objType = $parts[0]
$objName = $parts[1]
if (-not $typeDirMap.ContainsKey($objType)) {
Write-Error "Unknown object type: $objType"
exit 1
}
$dirName = $typeDirMap[$objType]
$bslFile = $null
if ($objType -eq "CommonModule") {
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Ext") "Module.bsl"
} elseif ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
# Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl
$formName = $parts[3]
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext") "Form") "Module.bsl"
} elseif ($parts.Count -ge 3) {
# Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl
$moduleName = $parts[2]
$moduleFileName = switch ($moduleName) {
"ObjectModule" { "ObjectModule.bsl" }
"ManagerModule" { "ManagerModule.bsl" }
"RecordSetModule" { "RecordSetModule.bsl" }
"CommandModule" { "CommandModule.bsl" }
default { "$moduleName.bsl" }
}
$bslFile = Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) (Join-Path "Ext" $moduleFileName)
} else {
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name"
exit 1
}
# --- Map InterceptorType to decorator ---
$decorator = switch ($InterceptorType) {
"Before" { "&Перед" }
"After" { "&После" }
"ModificationAndControl" { "&ИзменениеИКонтроль" }
}
# --- Map Context to annotation ---
$contextAnnotation = switch ($Context) {
"НаСервере" { "&НаСервере" }
"НаКлиенте" { "&НаКлиенте" }
"НаСервереБезКонтекста" { "&НаСервереБезКонтекста" }
default { "&$Context" }
}
# --- Procedure name ---
$procName = "${namePrefix}${MethodName}"
# --- Generate BSL code ---
$keyword = if ($IsFunction) { "Функция" } else { "Процедура" }
$endKeyword = if ($IsFunction) { "КонецФункции" } else { "КонецПроцедуры" }
$bodyLines = @()
switch ($InterceptorType) {
"Before" {
$bodyLines += "`t// TODO: код перед вызовом оригинального метода"
}
"After" {
$bodyLines += "`t// TODO: код после вызова оригинального метода"
}
"ModificationAndControl" {
$bodyLines += "`t// Скопируйте тело оригинального метода и внесите изменения,"
$bodyLines += "`t// используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки"
}
}
if ($IsFunction) {
$bodyLines += "`t"
$bodyLines += "`tВозврат Неопределено; // TODO: заменить на реальное возвращаемое значение"
}
$bslCode = @()
$bslCode += "$contextAnnotation"
$bslCode += "${decorator}(`"$MethodName`")"
$bslCode += "$keyword ${procName}()"
$bslCode += $bodyLines
$bslCode += "$endKeyword"
$bslText = ($bslCode -join "`r`n") + "`r`n"
# --- Check form borrowing for .Form. paths ---
if ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
$formName = $parts[3]
$dirName = $typeDirMap[$objType]
$formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") "${formName}.xml"
$formXmlFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
if (-not (Test-Path $formMetaFile) -or -not (Test-Path $formXmlFile)) {
Write-Host "[WARN] Form '$formName' metadata or Form.xml not found in extension."
Write-Host " Run /cfe-borrow first:"
Write-Host " /cfe-borrow -ExtensionPath $ExtensionPath -ConfigPath <ConfigPath> -Object `"$objType.$objName.Form.$formName`""
Write-Host ""
}
}
# --- Check if file exists and append ---
$bslDir = Split-Path $bslFile -Parent
if (-not (Test-Path $bslDir)) {
New-Item -ItemType Directory -Path $bslDir -Force | Out-Null
}
$enc = New-Object System.Text.UTF8Encoding($true)
if (Test-Path $bslFile) {
# Append to existing file
$existing = [System.IO.File]::ReadAllText($bslFile, $enc)
$separator = "`r`n"
if ($existing -and -not $existing.EndsWith("`n")) {
$separator = "`r`n`r`n"
}
$newContent = $existing + $separator + $bslText
[System.IO.File]::WriteAllText($bslFile, $newContent, $enc)
Write-Host "[OK] Добавлен перехватчик в существующий файл"
} else {
[System.IO.File]::WriteAllText($bslFile, $bslText, $enc)
Write-Host "[OK] Создан файл модуля"
}
Write-Host " Файл: $bslFile"
Write-Host " Декоратор: $decorator(`"$MethodName`")"
Write-Host " Процедура: ${procName}()"
Write-Host " Контекст: $contextAnnotation"
# cfe-patch-method v1.1 — Generate method interceptor for 1C extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$ExtensionPath,
[Parameter(Mandatory)]
[string]$ModulePath,
[Parameter(Mandatory)]
[string]$MethodName,
[Parameter(Mandatory)]
[ValidateSet("Before","After","ModificationAndControl")]
[string]$InterceptorType,
[string]$Context = "НаСервере",
[switch]$IsFunction
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve extension path ---
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
}
if (Test-Path $ExtensionPath -PathType Leaf) {
$ExtensionPath = Split-Path $ExtensionPath -Parent
}
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
if (-not (Test-Path $cfgFile)) {
Write-Error "Configuration.xml not found in: $ExtensionPath"
exit 1
}
# --- Read NamePrefix from Configuration.xml ---
$cfgDoc = New-Object System.Xml.XmlDocument
$cfgDoc.PreserveWhitespace = $false
$cfgDoc.Load($cfgFile)
$cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgDoc.NameTable)
$cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$propsNode = $cfgDoc.SelectSingleNode("//md:Configuration/md:Properties", $cfgNs)
$prefixNode = if ($propsNode) { $propsNode.SelectSingleNode("md:NamePrefix", $cfgNs) } else { $null }
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "Расш_" }
# --- Map ModulePath to file path ---
# ModulePath formats:
# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl
# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl
# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl
# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl
# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl
$typeDirMap = @{
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
"CommonModule"="CommonModules"; "Report"="Reports"; "DataProcessor"="DataProcessors"
"ExchangePlan"="ExchangePlans"; "ChartOfAccounts"="ChartsOfAccounts"
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
"AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters"
"Catalogs"="Catalogs"; "Documents"="Documents"; "Enums"="Enums"
"CommonModules"="CommonModules"; "Reports"="Reports"; "DataProcessors"="DataProcessors"
"ExchangePlans"="ExchangePlans"; "ChartsOfAccounts"="ChartsOfAccounts"
"ChartsOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
"ChartsOfCalculationTypes"="ChartsOfCalculationTypes"
"BusinessProcesses"="BusinessProcesses"; "Tasks"="Tasks"
"InformationRegisters"="InformationRegisters"; "AccumulationRegisters"="AccumulationRegisters"
"AccountingRegisters"="AccountingRegisters"; "CalculationRegisters"="CalculationRegisters"
}
$parts = $ModulePath.Split(".")
if ($parts.Count -lt 2) {
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module or CommonModule.Name"
exit 1
}
$objType = $parts[0]
$objName = $parts[1]
if (-not $typeDirMap.ContainsKey($objType)) {
Write-Error "Unknown object type: $objType"
exit 1
}
$dirName = $typeDirMap[$objType]
$bslFile = $null
if ($objType -eq "CommonModule") {
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Ext") "Module.bsl"
} elseif ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
# Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl
$formName = $parts[3]
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext") "Form") "Module.bsl"
} elseif ($parts.Count -ge 3) {
# Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl
$moduleName = $parts[2]
$moduleFileName = switch ($moduleName) {
"ObjectModule" { "ObjectModule.bsl" }
"ManagerModule" { "ManagerModule.bsl" }
"RecordSetModule" { "RecordSetModule.bsl" }
"CommandModule" { "CommandModule.bsl" }
default { "$moduleName.bsl" }
}
$bslFile = Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) (Join-Path "Ext" $moduleFileName)
} else {
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name"
exit 1
}
# --- Map InterceptorType to decorator ---
$decorator = switch ($InterceptorType) {
"Before" { "&Перед" }
"After" { "&После" }
"ModificationAndControl" { "&ИзменениеИКонтроль" }
}
# --- Map Context to annotation ---
$contextAnnotation = switch ($Context) {
"НаСервере" { "&НаСервере" }
"НаКлиенте" { "&НаКлиенте" }
"НаСервереБезКонтекста" { "&НаСервереБезКонтекста" }
default { "&$Context" }
}
# --- Procedure name ---
$procName = "${namePrefix}${MethodName}"
# --- Generate BSL code ---
$keyword = if ($IsFunction) { "Функция" } else { "Процедура" }
$endKeyword = if ($IsFunction) { "КонецФункции" } else { "КонецПроцедуры" }
$bodyLines = @()
switch ($InterceptorType) {
"Before" {
$bodyLines += "`t// TODO: код перед вызовом оригинального метода"
}
"After" {
$bodyLines += "`t// TODO: код после вызова оригинального метода"
}
"ModificationAndControl" {
$bodyLines += "`t// Скопируйте тело оригинального метода и внесите изменения,"
$bodyLines += "`t// используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки"
}
}
if ($IsFunction) {
$bodyLines += "`t"
$bodyLines += "`tВозврат Неопределено; // TODO: заменить на реальное возвращаемое значение"
}
$bslCode = @()
$bslCode += "$contextAnnotation"
$bslCode += "${decorator}(`"$MethodName`")"
$bslCode += "$keyword ${procName}()"
$bslCode += $bodyLines
$bslCode += "$endKeyword"
$bslText = ($bslCode -join "`r`n") + "`r`n"
# --- Check form borrowing for .Form. paths ---
if ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
$formName = $parts[3]
$dirName = $typeDirMap[$objType]
$formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") "${formName}.xml"
$formXmlFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
if (-not (Test-Path $formMetaFile) -or -not (Test-Path $formXmlFile)) {
Write-Host "[WARN] Form '$formName' metadata or Form.xml not found in extension."
Write-Host " Run /cfe-borrow first:"
Write-Host " /cfe-borrow -ExtensionPath $ExtensionPath -ConfigPath <ConfigPath> -Object `"$objType.$objName.Form.$formName`""
Write-Host ""
}
}
# --- Check if file exists and append ---
$bslDir = Split-Path $bslFile -Parent
if (-not (Test-Path $bslDir)) {
New-Item -ItemType Directory -Path $bslDir -Force | Out-Null
}
$enc = New-Object System.Text.UTF8Encoding($true)
if (Test-Path $bslFile) {
# Append to existing file
$existing = [System.IO.File]::ReadAllText($bslFile, $enc)
$separator = "`r`n"
if ($existing -and -not $existing.EndsWith("`n")) {
$separator = "`r`n`r`n"
}
$newContent = $existing + $separator + $bslText
[System.IO.File]::WriteAllText($bslFile, $newContent, $enc)
Write-Host "[OK] Добавлен перехватчик в существующий файл"
} else {
[System.IO.File]::WriteAllText($bslFile, $bslText, $enc)
Write-Host "[OK] Создан файл модуля"
}
Write-Host " Файл: $bslFile"
Write-Host " Декоратор: $decorator(`"$MethodName`")"
Write-Host " Процедура: ${procName}()"
Write-Host " Контекст: $contextAnnotation"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE)
# cfe-patch-method v1.1 — Generate method interceptor for 1C extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -84,6 +84,22 @@ def main():
"AccumulationRegister": "AccumulationRegisters",
"AccountingRegister": "AccountingRegisters",
"CalculationRegister": "CalculationRegisters",
"Catalogs": "Catalogs",
"Documents": "Documents",
"Enums": "Enums",
"CommonModules": "CommonModules",
"Reports": "Reports",
"DataProcessors": "DataProcessors",
"ExchangePlans": "ExchangePlans",
"ChartsOfAccounts": "ChartsOfAccounts",
"ChartsOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
"ChartsOfCalculationTypes": "ChartsOfCalculationTypes",
"BusinessProcesses": "BusinessProcesses",
"Tasks": "Tasks",
"InformationRegisters": "InformationRegisters",
"AccumulationRegisters": "AccumulationRegisters",
"AccountingRegisters": "AccountingRegisters",
"CalculationRegisters": "CalculationRegisters",
}
parts = module_path.split(".")
+3 -23
View File
@@ -17,33 +17,13 @@ allowed-tools:
| Параметр | Обяз. | Умолч. | Описание |
|---------------|:-----:|---------|-------------------------------------------------|
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
| Detailed | нет | — | Показывать [OK] для каждой проверки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл |
## Команда
```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"
```
## Проверки (13 шагов)
| # | Проверка | Уровень |
|---|----------|---------|
| 1 | XML well-formedness, MetaDataObject/Configuration, version | ERROR |
| 2 | InternalInfo: 7 ContainedObject, валидные ClassId | ERROR |
| 3 | Extension properties: ObjectBelonging=Adopted, Name, Purpose, NamePrefix, KeepMapping | ERROR |
| 4 | Enum-значения: ConfigurationExtensionCompatibilityMode, DefaultRunMode, ScriptVariant, InterfaceCompatibilityMode | ERROR |
| 5 | ChildObjects: валидные типы (44), нет дубликатов, каноничный порядок | ERROR/WARN |
| 6 | DefaultLanguage ссылается на Language в ChildObjects | ERROR |
| 7 | Файлы языков существуют | WARN |
| 8 | Каталоги объектов существуют | WARN |
| 9 | Заимствованные объекты: ObjectBelonging=Adopted, ExtendedConfigurationObject UUID | ERROR/WARN |
| 10 | Sub-items: Attribute, TabularSection (InternalInfo + вложенные), EnumValue, Form-ссылки | ERROR |
| 11 | Заимствованные формы: метаданные, Form.xml, Module.bsl, BaseForm version | ERROR/WARN |
| 12 | Зависимости форм: CommonPicture, StyleItem (с whitelist платформенных), Enum DesignTimeRef | WARN |
| 13 | TypeLink: human-readable Items.* DataPath (должны быть удалены) | WARN |
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
@@ -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
```
@@ -1,4 +1,4 @@
# db-load-xml v1.1 — Load 1C configuration from XML files
# db-load-xml v1.3 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
@@ -98,7 +98,10 @@ param(
[string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)]
[switch]$UpdateDB
[switch]$UpdateDB,
[Parameter(Mandatory=$false)]
[switch]$StrictLog
)
$OutputEncoding = [System.Text.Encoding]::UTF8
@@ -213,20 +216,58 @@ try {
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Read log ---
$logContent = $null
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
}
# --- Scan log for silent rejections ---
# Platform often writes load-time rejections into /Out but exits with code 0.
# These patterns flag cases where metadata was dropped or rejected silently.
$fatalLogPatterns = @(
'Неверное свойство объекта метаданных',
'не входит в состав объекта метаданных',
'Неизвестное имя типа',
'Неизвестный объект метаданных',
'Ни один из документов не является регистратором для регистра',
'Неверное значение перечисления',
'не может быть приведен к типу'
)
$silentFailures = @()
if ($logContent) {
foreach ($line in ($logContent -split "`r?`n")) {
foreach ($pat in $fatalLogPatterns) {
if ($line -match [regex]::Escape($pat)) {
$silentFailures += $line.Trim()
break
}
}
}
}
# --- Result ---
# Default: mirror platform's verdict via exit code. Log content (including any
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
if ($silentFailures.Count -gt 0) {
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" }
Write-Host $msg -ForegroundColor Yellow
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
}
exit $exitCode
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-xml v1.1 — Load 1C configuration from XML files
# db-load-xml v1.3 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -63,6 +63,11 @@ def main():
help="File format (default: Hierarchical)",
)
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
parser.add_argument(
"-StrictLog",
action="store_true",
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
)
args = parser.parse_args()
# --- Resolve V8Path ---
@@ -157,22 +162,60 @@ def main():
)
exit_code = result.returncode
# --- Read log ---
log_content = ""
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
except Exception:
log_content = ""
# --- Scan log for silent rejections ---
# Platform often writes load-time rejections into /Out but exits with code 0.
# These patterns flag cases where metadata was dropped or rejected silently.
fatal_log_patterns = [
"Неверное свойство объекта метаданных",
"не входит в состав объекта метаданных",
"Неизвестное имя типа",
"Неизвестный объект метаданных",
"Ни один из документов не является регистратором для регистра",
"Неверное значение перечисления",
"не может быть приведен к типу",
]
silent_failures = []
if log_content:
for line in log_content.splitlines():
for pat in fatal_log_patterns:
if pat in line:
silent_failures.append(line.strip())
break
# --- Result ---
# Default: mirror platform's verdict via exit code. Log content (including any
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
if exit_code == 0:
print("Load completed successfully")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
if silent_failures:
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
print(
f"[warning] log contains {len(silent_failures)} rejection(s) — "
f"platform loaded config but dropped properties/refs{suffix}",
file=sys.stderr,
)
for f in silent_failures:
print(f" {f}", file=sys.stderr)
if args.StrictLog and exit_code == 0:
exit_code = 1
sys.exit(exit_code)
+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,207 +0,0 @@
# epf-add-form v1.0 — 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"
# --- Проверки ---
$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="2.17">
<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="2.17">
<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,251 +0,0 @@
#!/usr/bin/env python3
# add-form v1.0 — Add managed form to 1C external data processor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import uuid
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
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
# --- 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"'
' version="2.17">\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"'
' version="2.17">\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()
+2 -3
View File
@@ -1,6 +1,6 @@
---
name: epf-bsp-add-command
description: Добавить команду в дополнительную обработку БСП
description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
allowed-tools:
- Read
@@ -191,7 +191,6 @@ allowed-tools:
НоваяКоманда.Идентификатор = "ЗаказПокупателя";
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
НоваяКоманда.ПоказыватьОповещение = Ложь;
НоваяКоманда.Модификатор = "ПечатьMXL";
```
И в существующую процедуру `Печать` добавится блок обработки.
И в существующую процедуру `ВыполнитьКоманду` добавится блок обработки.
+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 -17
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,26 +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>"]
```
## Что создаётся
```
<SrcDir>/
├── <Name>.xml # Корневой файл метаданных (4 UUID)
└── <Name>/
└── Ext/
└── ObjectModule.bsl # Модуль объекта с 3 регионами
```
- Корневой XML содержит `MetaDataObject/ExternalDataProcessor` с пустыми `DefaultForm` и `ChildObjects`
- ClassId фиксирован: `c3831ec8-d8d5-4f93-8a22-f9bfae07327f`
- Файл создаётся в UTF-8 с BOM
## Дальнейшие шаги
- Добавить форму: `/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
+3 -19
View File
@@ -17,30 +17,14 @@ allowed-tools:
| Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------|
| ObjectPath | да | — | Путь к корневому XML или каталогу обработки |
| Detailed | нет | — | Показывать [OK] для каждой проверки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда
```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 | Root structure: MetaDataObject/ExternalDataProcessor | ERROR |
| 2 | InternalInfo: ClassId, ContainedObject, GeneratedType | ERROR / WARN |
| 3 | Properties: Name (identifier), Synonym | ERROR / WARN |
| 4 | ChildObjects: допустимые типы, порядок | ERROR / WARN |
| 5 | Cross-references: DefaultForm → Form, AuxiliaryForm | ERROR / WARN |
| 6 | Attributes: UUID, Name, Type | ERROR |
| 7 | TabularSections: UUID, Name, GeneratedType, Attributes | ERROR / WARN |
| 8 | Уникальность имён (Attribute, TS, Form, Template, Command) | ERROR |
| 9 | Файлы: формы (.xml + Ext/Form.xml), макеты | ERROR |
| 10 | Дескрипторы форм: корневая структура, uuid, Name, FormType | ERROR / WARN |
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
@@ -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 -28
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,35 +31,9 @@ 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]
```
## Что создаётся
```
<SrcDir>/
├── <Name>.xml # Корневой файл метаданных (4 UUID)
└── <Name>/
└── Ext/
└── ObjectModule.bsl # Модуль объекта с 3 регионами
```
При `--WithSKD` дополнительно:
```
<SrcDir>/<Name>/
Templates/
├── ОсновнаяСхемаКомпоновкиДанных.xml # Метаданные макета
└── ОсновнаяСхемаКомпоновкиДанных/
└── Ext/
└── Template.xml # Пустая СКД
```
- Корневой XML содержит `MetaDataObject/ExternalReport` с пустыми `DefaultForm`, `MainDataCompositionSchema` и `ChildObjects`
- При `--WithSKD``MainDataCompositionSchema` заполняется ссылкой на макет, `ChildObjects` содержит `<Template>`
- ClassId фиксирован: `e41aff26-25cf-4bb6-b6c1-3f478a75f374`
- Файл создаётся в UTF-8 с BOM
## Дальнейшие шаги
- Добавить форму: `/form-add`
+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
+3 -19
View File
@@ -19,30 +19,14 @@ allowed-tools:
| Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------|
| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта |
| Detailed | нет | — | Показывать [OK] для каждой проверки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда
```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"
```
## Проверки
| # | Проверка | Серьёзность |
|----|-------------------------------------------------------|--------------|
| 1 | Root structure: MetaDataObject/ExternalReport | ERROR |
| 2 | InternalInfo: ClassId, ContainedObject, GeneratedType | ERROR / WARN |
| 3 | Properties: Name, Synonym, MainDataCompositionSchema | ERROR / WARN |
| 4 | ChildObjects: допустимые типы, порядок | ERROR / WARN |
| 5 | Cross-references: DefaultForm, MainDCS → Template | ERROR / WARN |
| 6 | Attributes: UUID, Name, Type | ERROR |
| 7 | TabularSections: UUID, Name, GeneratedType, Attributes | ERROR / WARN |
| 8 | Уникальность имён (Attribute, TS, Form, Template, Command) | ERROR |
| 9 | Файлы: формы (.xml + Ext/Form.xml), макеты | ERROR |
| 10 | Дескрипторы форм: корневая структура, uuid, Name, FormType | ERROR / WARN |
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
+2 -22
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 — назначение формы
@@ -44,26 +44,6 @@ powershell.exe -NoProfile -File .claude/skills/form-add/scripts/form-add.ps1 -Ob
| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm |
| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm |
## Что создаётся
```
<ObjectDir>/Forms/
├── <FormName>.xml # Метаданные формы (UUID)
└── <FormName>/
└── Ext/
├── Form.xml # Описание формы (logform namespace)
└── Form/
└── Module.bsl # BSL-модуль с 5 регионами + ПриСозданииНаСервере
```
## Что модифицируется
- `<ObjectPath>` — добавляется `<Form>` в `ChildObjects` (перед `<Template>` или `<TabularSection>`), обновляется Default*Form (автоматически если пустое, или явно при `--set-default`)
## Поддерживаемые типы объектов
Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, InformationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ExchangePlan, BusinessProcess, Task
## Примеры
```
+478 -448
View File
@@ -1,448 +1,478 @@
# form-add v1.0 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$ObjectPath,
[Parameter(Mandatory)]
[string]$FormName,
[string]$Synonym = $FormName,
[string]$Purpose = "Object",
[switch]$SetDefault
)
$ErrorActionPreference = "Stop"
# --- Фаза 1: Определение типа объекта ---
if (-not (Test-Path $ObjectPath)) {
Write-Error "Файл объекта не найден: $ObjectPath"
exit 1
}
$objectXmlFull = Resolve-Path $ObjectPath
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load($objectXmlFull.Path)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
# Определяем тип объекта по корневому тегу внутри MetaDataObject
$metaDataObject = $xmlDoc.SelectSingleNode("//md:MetaDataObject", $nsMgr)
if (-not $metaDataObject) {
# Пробуем без namespace (fallback)
$metaDataObject = $xmlDoc.DocumentElement
}
$supportedTypes = @(
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task"
)
$objectType = $null
$objectNode = $null
foreach ($t in $supportedTypes) {
$node = $xmlDoc.SelectSingleNode("//md:$t", $nsMgr)
if ($node) {
$objectType = $t
$objectNode = $node
break
}
}
if (-not $objectType) {
Write-Error "Не удалось определить тип объекта. Поддерживаемые типы: $($supportedTypes -join ', ')"
exit 1
}
# Имя объекта из Properties/Name
$objectName = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:Name", $nsMgr).InnerText
if (-not $objectName) {
Write-Error "Не удалось определить имя объекта из Properties/Name"
exit 1
}
Write-Host ""
Write-Host "=== form-add ==="
Write-Host ""
Write-Host "Object: $objectType.$objectName"
# --- Фаза 2: Валидация Purpose ---
$Purpose = $Purpose.Substring(0,1).ToUpper() + $Purpose.Substring(1).ToLower()
# Нормализация
switch ($Purpose) {
"Object" { }
"List" { }
"Choice" { }
"Record" { }
default {
Write-Error "Недопустимое назначение: $Purpose. Допустимые: Object, List, Choice, Record"
exit 1
}
}
$objectLikeTypes = @("Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes", "ExchangePlan", "BusinessProcess", "Task")
$processorLikeTypes = @("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport")
switch ($Purpose) {
"Object" {
# допустимо для всех типов
}
"List" {
if ($objectType -eq "DataProcessor") {
Write-Error "Purpose=List недопустим для DataProcessor"
exit 1
}
}
"Choice" {
if ($objectType -in $processorLikeTypes -or $objectType -eq "InformationRegister") {
Write-Error "Purpose=Choice недопустим для $objectType"
exit 1
}
}
"Record" {
if ($objectType -ne "InformationRegister") {
Write-Error "Purpose=Record допустим только для InformationRegister"
exit 1
}
}
}
# --- Фаза 3: Создание файлов ---
$objectDir = [System.IO.Path]::ChangeExtension($objectXmlFull.Path, $null).TrimEnd('.')
$formsDir = Join-Path $objectDir "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)
# --- 3a. Метаданные формы ---
$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="2.17">
<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)
# --- 3b. Form.xml ---
$formXmlPath = Join-Path $formExtDir "Form.xml"
$formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
# Динамический список
# MainTable: тип.имя
$mainTable = "$objectType.$objectName"
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="2.17">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<Events>
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
</Events>
<ChildItems/>
<Attributes>
<Attribute name="Список" id="1">
<Type>
<v8:Type>cfg:DynamicList</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>
<Settings xsi:type="DynamicList">
<MainTable>$mainTable</MainTable>
</Settings>
</Attribute>
</Attributes>
</Form>
"@
} elseif ($Purpose -eq "Record") {
# Запись регистра сведений
$mainAttrName = "Запись"
$mainAttrType = "InformationRegisterRecordManager.$objectName"
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="2.17">
<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>
</Attribute>
</Attributes>
</Form>
"@
} else {
# Object — форма объекта
$mainAttrName = "Объект"
# Маппинг типа объекта на тип реквизита
$attrTypeMap = @{
"Document" = "DocumentObject"
"Catalog" = "CatalogObject"
"DataProcessor" = "DataProcessorObject"
"Report" = "ReportObject"
"ExternalDataProcessor" = "ExternalDataProcessorObject"
"ExternalReport" = "ExternalReportObject"
"ChartOfAccounts" = "ChartOfAccountsObject"
"ChartOfCharacteristicTypes" = "ChartOfCharacteristicTypesObject"
"ExchangePlan" = "ExchangePlanObject"
"BusinessProcess" = "BusinessProcessObject"
"Task" = "TaskObject"
"InformationRegister" = "InformationRegisterRecordManager"
}
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="2.17">
<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>
</Attribute>
</Attributes>
</Form>
"@
}
if (Test-Path $formXmlPath) {
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
} else {
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
}
# --- 3c. Module.bsl ---
$modulePath = Join-Path $formModuleDir "Module.bsl"
$moduleBsl = @"
#Область ОбработчикиСобытийФормы
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
КонецПроцедуры
#КонецОбласти
#Область ОбработчикиСобытийЭлементовФормы
#КонецОбласти
#Область ОбработчикиКомандФормы
#КонецОбласти
#Область ОбработчикиОповещений
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
"@
if (Test-Path $modulePath) {
Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting"
} else {
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
}
# --- Фаза 4: Регистрация в родительском объекте ---
$childObjects = $xmlDoc.SelectSingleNode("//md:${objectType}/md:ChildObjects", $nsMgr)
if (-not $childObjects) {
Write-Error "Не найден элемент ChildObjects в $ObjectPath"
exit 1
}
# Добавить <Form>$FormName</Form>
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formElem.InnerText = $FormName
# Ищем первый <Template> для вставки перед ним
$firstTemplate = $childObjects.SelectSingleNode("md:Template", $nsMgr)
# Ищем первую <TabularSection> для вставки перед ней (если нет Template)
$firstTabular = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr)
# Определяем точку вставки: перед Template, перед TabularSection, или в конец
$insertBefore = $null
if ($firstTemplate) {
$insertBefore = $firstTemplate
} elseif ($firstTabular) {
$insertBefore = $firstTabular
}
if ($insertBefore) {
# Вставить перед найденным элементом, с переносом строки
$whitespace = $xmlDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($formElem, $insertBefore) | Out-Null
$childObjects.InsertBefore($whitespace, $formElem) | Out-Null
# Переставляем: whitespace перед formElem — неправильный порядок
# Правильно: formElem, затем whitespace перед insertBefore
# InsertBefore возвращает вставленный узел, порядок: ... formElem whitespace insertBefore ...
# На самом деле нам нужно: ... \n\t\t\tformElem \n\t\t\tinsertBefore
# Удалим и вставим правильно
$childObjects.RemoveChild($whitespace) | Out-Null
$childObjects.RemoveChild($formElem) | Out-Null
$childObjects.InsertBefore($formElem, $insertBefore) | Out-Null
# Whitespace нужен ДО formElem (перенос строки + отступ)
# Но перед insertBefore уже должен быть whitespace от предыдущего элемента
# Нам нужно добавить whitespace ПОСЛЕ formElem (перед insertBefore)
$ws = $xmlDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($ws, $insertBefore) | Out-Null
} else {
# Добавить в конец 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
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
}
}
}
# --- SetDefault ---
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
$isFirstFormForPurpose = $false
$defaultPropName = $null
$defaultValue = "$objectType.$objectName.Form.$FormName"
# Определяем имя свойства для DefaultForm
switch ($Purpose) {
"Object" {
if ($objectType -in $processorLikeTypes) {
$defaultPropName = "DefaultForm"
} else {
$defaultPropName = "DefaultObjectForm"
}
}
"List" { $defaultPropName = "DefaultListForm" }
"Choice" { $defaultPropName = "DefaultChoiceForm" }
"Record" { $defaultPropName = "DefaultRecordForm" }
}
# Проверяем, установлено ли уже значение
$defaultNode = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:$defaultPropName", $nsMgr)
if ($defaultNode) {
$isFirstFormForPurpose = [string]::IsNullOrWhiteSpace($defaultNode.InnerText)
}
$defaultUpdated = $false
if ($SetDefault -or $isFirstFormForPurpose) {
if ($defaultNode) {
$defaultNode.InnerText = $defaultValue
$defaultUpdated = $true
}
}
# Сохранить с BOM
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom
$settings.Indent = $false
$stream = New-Object System.IO.FileStream($objectXmlFull.Path, [System.IO.FileMode]::Create)
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
$xmlDoc.Save($writer)
$writer.Close()
$stream.Close()
# --- Фаза 5: Вывод ---
# Относительные пути для вывода
$basePath = Split-Path $objectXmlFull.Path -Parent
# Определяем корень (ищем родительский каталог типа Documents, Catalogs и т.д.)
$relFormMeta = $formMetaPath.Replace($basePath, "").TrimStart("\", "/")
$relFormXml = $formXmlPath.Replace($basePath, "").TrimStart("\", "/")
$relModule = $modulePath.Replace($basePath, "").TrimStart("\", "/")
$objFileName = [System.IO.Path]::GetFileName($ObjectPath)
$objDirName = Split-Path $ObjectPath -Parent
$objBaseName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath)
Write-Host "Created:"
Write-Host " Metadata: $objDirName\$objBaseName\Forms\$FormName.xml"
Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml"
Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
Write-Host ""
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
if ($defaultUpdated) {
Write-Host "${defaultPropName}: $defaultValue"
}
Write-Host ""
# form-add v1.5 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$ObjectPath,
[Parameter(Mandatory)]
[string]$FormName,
[string]$Synonym = $FormName,
[string]$Purpose = "Object",
[switch]$SetDefault
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Detect XML 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"
}
# --- Фаза 1: Определение типа объекта ---
# Resolve ObjectPath (directory → .xml)
if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) {
$ObjectPath = Join-Path (Get-Location).Path $ObjectPath
}
if (Test-Path $ObjectPath -PathType Container) {
$dirName = Split-Path $ObjectPath -Leaf
$candidate = Join-Path $ObjectPath "$dirName.xml"
$sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml"
if (Test-Path $candidate) { $ObjectPath = $candidate }
elseif (Test-Path $sibling) { $ObjectPath = $sibling }
}
if (-not (Test-Path $ObjectPath)) {
Write-Error "Файл объекта не найден: $ObjectPath"
exit 1
}
$objectXmlFull = Resolve-Path $ObjectPath
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent)
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load($objectXmlFull.Path)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
# Определяем тип объекта по корневому тегу внутри MetaDataObject
$metaDataObject = $xmlDoc.SelectSingleNode("//md:MetaDataObject", $nsMgr)
if (-not $metaDataObject) {
# Пробуем без namespace (fallback)
$metaDataObject = $xmlDoc.DocumentElement
}
$supportedTypes = @(
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task"
)
$objectType = $null
$objectNode = $null
foreach ($t in $supportedTypes) {
$node = $xmlDoc.SelectSingleNode("//md:$t", $nsMgr)
if ($node) {
$objectType = $t
$objectNode = $node
break
}
}
if (-not $objectType) {
Write-Error "Не удалось определить тип объекта. Поддерживаемые типы: $($supportedTypes -join ', ')"
exit 1
}
# Имя объекта из Properties/Name
$objectName = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:Name", $nsMgr).InnerText
if (-not $objectName) {
Write-Error "Не удалось определить имя объекта из Properties/Name"
exit 1
}
Write-Host ""
Write-Host "=== form-add ==="
Write-Host ""
Write-Host "Object: $objectType.$objectName"
# --- Фаза 2: Валидация Purpose ---
$Purpose = $Purpose.Substring(0,1).ToUpper() + $Purpose.Substring(1).ToLower()
# Нормализация
switch ($Purpose) {
"Object" { }
"List" { }
"Choice" { }
"Record" { }
default {
Write-Error "Недопустимое назначение: $Purpose. Допустимые: Object, List, Choice, Record"
exit 1
}
}
$objectLikeTypes = @("Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes", "ExchangePlan", "BusinessProcess", "Task")
$processorLikeTypes = @("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport")
switch ($Purpose) {
"Object" {
# допустимо для всех типов
}
"List" {
if ($objectType -eq "DataProcessor") {
Write-Error "Purpose=List недопустим для DataProcessor"
exit 1
}
}
"Choice" {
if ($objectType -in $processorLikeTypes -or $objectType -eq "InformationRegister") {
Write-Error "Purpose=Choice недопустим для $objectType"
exit 1
}
}
"Record" {
if ($objectType -ne "InformationRegister") {
Write-Error "Purpose=Record допустим только для InformationRegister"
exit 1
}
}
}
# --- Фаза 3: Создание файлов ---
$objectDir = [System.IO.Path]::ChangeExtension($objectXmlFull.Path, $null).TrimEnd('.')
$formsDir = Join-Path $objectDir "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)
# --- 3a. Метаданные формы ---
$formUuid = [guid]::NewGuid().ToString()
# ExtendedPresentation — only for DataProcessor, Report, ExternalDataProcessor, ExternalReport forms
$extPresentationLine = ""
if ($objectType -in $processorLikeTypes) {
$extPresentationLine = "`n`t`t`t<ExtendedPresentation/>"
}
$formMetaXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$($script:formatVersion)">
<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>$extPresentationLine
</Properties>
</Form>
</MetaDataObject>
"@
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
# --- 3b. Form.xml ---
$formXmlPath = Join-Path $formExtDir "Form.xml"
$formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
# Динамический список
# MainTable: тип.имя
$mainTable = "$objectType.$objectName"
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<ChildItems/>
<Attributes>
<Attribute name="Список" id="1">
<Type>
<v8:Type>cfg:DynamicList</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>
<Settings xsi:type="DynamicList">
<MainTable>$mainTable</MainTable>
</Settings>
</Attribute>
</Attributes>
</Form>
"@
} elseif ($Purpose -eq "Record") {
# Запись регистра сведений
$mainAttrName = "Запись"
$mainAttrType = "InformationRegisterRecordManager.$objectName"
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<ChildItems/>
<Attributes>
<Attribute name="$mainAttrName" id="1">
<Type>
<v8:Type>cfg:$mainAttrType</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>
<SavedData>true</SavedData>
</Attribute>
</Attributes>
</Form>
"@
} else {
# Object — форма объекта
$mainAttrName = "Объект"
# Маппинг типа объекта на тип реквизита
$attrTypeMap = @{
"Document" = "DocumentObject"
"Catalog" = "CatalogObject"
"DataProcessor" = "DataProcessorObject"
"Report" = "ReportObject"
"ExternalDataProcessor" = "ExternalDataProcessorObject"
"ExternalReport" = "ExternalReportObject"
"ChartOfAccounts" = "ChartOfAccountsObject"
"ChartOfCharacteristicTypes" = "ChartOfCharacteristicTypesObject"
"ExchangePlan" = "ExchangePlanObject"
"BusinessProcess" = "BusinessProcessObject"
"Task" = "TaskObject"
"InformationRegister" = "InformationRegisterRecordManager"
"AccumulationRegister" = "AccumulationRegisterRecordSet"
}
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
$savedDataLine = ""
if ($objectType -notin $processorLikeTypes) {
$savedDataLine = "`n`t`t`t<SavedData>true</SavedData>"
}
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<ChildItems/>
<Attributes>
<Attribute name="$mainAttrName" id="1">
<Type>
<v8:Type>cfg:$mainAttrType</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>$savedDataLine
</Attribute>
</Attributes>
</Form>
"@
}
if (Test-Path $formXmlPath) {
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
} else {
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
}
# --- 3c. Module.bsl ---
$modulePath = Join-Path $formModuleDir "Module.bsl"
$moduleBsl = @"
#Область ОбработчикиСобытийФормы
#КонецОбласти
#Область ОбработчикиСобытийЭлементовФормы
#КонецОбласти
#Область ОбработчикиКомандФормы
#КонецОбласти
#Область ОбработчикиОповещений
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
"@
if (Test-Path $modulePath) {
Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting"
} else {
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
}
# --- Фаза 4: Регистрация в родительском объекте ---
$childObjects = $xmlDoc.SelectSingleNode("//md:${objectType}/md:ChildObjects", $nsMgr)
if (-not $childObjects) {
Write-Error "Не найден элемент ChildObjects в $ObjectPath"
exit 1
}
# Добавить <Form>$FormName</Form>
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formElem.InnerText = $FormName
# Ищем первый <Template> для вставки перед ним
$firstTemplate = $childObjects.SelectSingleNode("md:Template", $nsMgr)
# Ищем первую <TabularSection> для вставки перед ней (если нет Template)
$firstTabular = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr)
# Определяем точку вставки: перед Template, перед TabularSection, или в конец
$insertBefore = $null
if ($firstTemplate) {
$insertBefore = $firstTemplate
} elseif ($firstTabular) {
$insertBefore = $firstTabular
}
if ($insertBefore) {
# Вставить перед найденным элементом, с переносом строки
$whitespace = $xmlDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($formElem, $insertBefore) | Out-Null
$childObjects.InsertBefore($whitespace, $formElem) | Out-Null
# Переставляем: whitespace перед formElem — неправильный порядок
# Правильно: formElem, затем whitespace перед insertBefore
# InsertBefore возвращает вставленный узел, порядок: ... formElem whitespace insertBefore ...
# На самом деле нам нужно: ... \n\t\t\tformElem \n\t\t\tinsertBefore
# Удалим и вставим правильно
$childObjects.RemoveChild($whitespace) | Out-Null
$childObjects.RemoveChild($formElem) | Out-Null
$childObjects.InsertBefore($formElem, $insertBefore) | Out-Null
# Whitespace нужен ДО formElem (перенос строки + отступ)
# Но перед insertBefore уже должен быть whitespace от предыдущего элемента
# Нам нужно добавить whitespace ПОСЛЕ formElem (перед insertBefore)
$ws = $xmlDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($ws, $insertBefore) | Out-Null
} else {
# Добавить в конец 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
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
}
}
}
# --- SetDefault ---
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
$isFirstFormForPurpose = $false
$defaultPropName = $null
$defaultValue = "$objectType.$objectName.Form.$FormName"
# Определяем имя свойства для DefaultForm
switch ($Purpose) {
"Object" {
if ($objectType -in $processorLikeTypes) {
$defaultPropName = "DefaultForm"
} else {
$defaultPropName = "DefaultObjectForm"
}
}
"List" { $defaultPropName = "DefaultListForm" }
"Choice" { $defaultPropName = "DefaultChoiceForm" }
"Record" { $defaultPropName = "DefaultRecordForm" }
}
# Проверяем, установлено ли уже значение
$defaultNode = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:$defaultPropName", $nsMgr)
if ($defaultNode) {
$isFirstFormForPurpose = [string]::IsNullOrWhiteSpace($defaultNode.InnerText)
}
$defaultUpdated = $false
if ($SetDefault -or $isFirstFormForPurpose) {
if ($defaultNode) {
$defaultNode.InnerText = $defaultValue
$defaultUpdated = $true
}
}
# Сохранить с BOM
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom
$settings.Indent = $false
$stream = New-Object System.IO.FileStream($objectXmlFull.Path, [System.IO.FileMode]::Create)
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
$xmlDoc.Save($writer)
$writer.Close()
$stream.Close()
# --- Фаза 5: Вывод ---
# Относительные пути для вывода
$basePath = Split-Path $objectXmlFull.Path -Parent
# Определяем корень (ищем родительский каталог типа Documents, Catalogs и т.д.)
$relFormMeta = $formMetaPath.Replace($basePath, "").TrimStart("\", "/")
$relFormXml = $formXmlPath.Replace($basePath, "").TrimStart("\", "/")
$relModule = $modulePath.Replace($basePath, "").TrimStart("\", "/")
$objFileName = [System.IO.Path]::GetFileName($ObjectPath)
$objDirName = Split-Path $ObjectPath -Parent
$objBaseName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath)
Write-Host "Created:"
Write-Host " Metadata: $objDirName\$objBaseName\Forms\$FormName.xml"
Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml"
Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
Write-Host ""
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
if ($defaultUpdated) {
Write-Host "${defaultPropName}: $defaultValue"
}
Write-Host ""
+47 -25
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
# form-add v1.0 — 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
import os
import re
import sys
import uuid
@@ -15,10 +16,28 @@ NSMAP = {
}
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"encoding='UTF-8'", b'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)
@@ -49,13 +68,24 @@ def main():
# --- Phase 1: Determine object type ---
# Resolve ObjectPath (directory → .xml)
if not os.path.isabs(object_path):
object_path = os.path.join(os.getcwd(), object_path)
if os.path.isdir(object_path):
object_path = object_path.rstrip("/\\") + ".xml"
dir_name = os.path.basename(object_path.rstrip("/\\"))
candidate = os.path.join(object_path, dir_name + ".xml")
sibling = os.path.join(os.path.dirname(object_path.rstrip("/\\")), dir_name + ".xml")
if os.path.isfile(candidate):
object_path = candidate
elif os.path.isfile(sibling):
object_path = sibling
if not os.path.isfile(object_path):
print(f"Файл объекта не найден: {object_path}", file=sys.stderr)
sys.exit(1)
object_xml_full = os.path.abspath(object_path)
format_version = detect_format_version(os.path.dirname(object_xml_full))
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(object_xml_full, parser_xml)
root = tree.getroot()
@@ -63,7 +93,7 @@ def main():
supported_types = [
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task",
]
@@ -160,7 +190,7 @@ def main():
' 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">\n'
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'
@@ -177,8 +207,8 @@ def main():
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n'
'\t\t\t<ExtendedPresentation/>\n'
'\t\t</Properties>\n'
+ ('\t\t\t<ExtendedPresentation/>\n' if object_type in processor_like_types else '')
+ '\t\t</Properties>\n'
'\t</Form>\n'
'</MetaDataObject>'
)
@@ -214,13 +244,10 @@ def main():
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\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'
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
@@ -243,13 +270,10 @@ def main():
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\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'
@@ -280,19 +304,22 @@ def main():
"BusinessProcess": "BusinessProcessObject",
"Task": "TaskObject",
"InformationRegister": "InformationRegisterRecordManager",
"AccumulationRegister": "AccumulationRegisterRecordSet",
}
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
saved_data_line = ''
if object_type not in processor_like_types:
saved_data_line = '\t\t\t<SavedData>true</SavedData>\n'
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\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'
@@ -300,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>'
@@ -318,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'
+153 -27
View File
@@ -1,7 +1,7 @@
---
name: form-compile
description: Компиляция управляемой формы 1С из компактного JSON-определения. Используй когда нужно создать форму с нуля по описанию элементов
argument-hint: <JsonPath> <OutputPath>
description: Компиляция управляемой формы 1С из JSON-определения или из метаданных объекта. Используй когда нужно создать форму с нуля по описанию элементов или сгенерировать типовую форму
argument-hint: <JsonPath> <OutputPath> | -FromObject <OutputPath>
allowed-tools:
- Bash
- Read
@@ -9,29 +9,30 @@ allowed-tools:
- Glob
---
# /form-compile — Генерация Form.xml из JSON DSL
# /form-compile — Генерация Form.xml
Принимает компактное JSON-определение формы (20–50 строк) и генерирует полный корректный Form.xml (100500+ строк) с namespace-декларациями, автогенерированными companion-элементами, последовательными ID.
Два режима:
1. **JSON DSL** — из JSON-определения формы
2. **From object** (`-FromObject`) — автоматически из метаданных объекта 1С по пресету ERP
> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника: архетипы, конвенции именования, продвинутые паттерны. Для простых форм (1–3 поля, пользователь описал что нужно) — не нужно.
## Использование
```
/form-compile <JsonPath> <OutputPath>
```
> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника. Для простых форм (1–3 поля) — не нужно.
## Параметры
| Параметр | Обязательный | Описание |
|------------|:------------:|-----------------------------------|
| JsonPath | да | Путь к JSON-определению формы |
| OutputPath | да | Путь к выходному файлу Form.xml |
| Параметр | Обязательный | Описание |
|------------|:------------:|---------------------------------|
| JsonPath | режим 1 | Путь к JSON-определению формы |
| OutputPath | да | Путь к выходному Form.xml |
| FromObject | режим 2 | Флаг (без значения) — генерация по метаданным объекта |
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -JsonPath "<json>" -OutputPath "<xml>"
# Режим JSON DSL
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_SKILL_DIR}/scripts/form-compile.ps1" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
```
## JSON DSL — справка
@@ -61,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 | имя |
@@ -73,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 | имя |
### Общие свойства (все типы элементов)
@@ -95,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`
@@ -123,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)
@@ -133,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)
| Ключ | Описание |
@@ -148,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: [...]` | Вложенные элементы |
@@ -167,6 +204,41 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `footer: true` | Показать подвал |
| `commandBarLocation` | `"None"`, `"Top"`, `"Auto"` |
| `searchStringLocation` | `"None"`, `"Top"`, `"Auto"` |
| `choiceMode: true` | Режим выбора (для форм выбора) |
| `initialTreeView` | `"ExpandTopLevel"` и др. (иерархические списки) |
| `enableDrag: true` | Разрешить перетаскивание |
| `enableStartDrag: true` | Разрешить начало перетаскивания |
| `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)
@@ -188,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)
| Ключ | Описание |
@@ -221,6 +314,9 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
```json
{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true }
{ "name": "Список", "type": "DynamicList", "main": true, "settings": {
"mainTable": "Catalog.Номенклатура", "dynamicDataRead": true
}}
{ "name": "Итого", "type": "decimal(15,2)" }
{ "name": "Таблица", "type": "ValueTable", "columns": [
{ "name": "Номенклатура", "type": "CatalogRef.Номенклатура" },
@@ -229,6 +325,7 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
```
- `savedData: true` — сохраняемые данные
- `main: true` — главный реквизит формы (например, основной `*Object.*`, `DynamicList`, `*RecordSet.*`)
### Команды (commands)
@@ -241,6 +338,8 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
### Система типов
**Примитивные:**
| DSL | XML |
|------------------------|----------------------------------------|
| `"string"` / `"string(100)"` | `xs:string` + StringQualifiers |
@@ -248,11 +347,38 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `"decimal(10,0,nonneg)"` | с AllowedSign=Nonnegative |
| `"boolean"` | `xs:boolean` |
| `"date"` / `"dateTime"` / `"time"` | `xs:dateTime` + DateFractions |
| `"CatalogRef.XXX"` | `cfg:CatalogRef.XXX` |
| `"DocumentRef.XXX"` | `cfg:DocumentRef.XXX` |
| `"ValueTable"` | `v8:ValueTable` |
| `"ValueList"` | `v8:ValueListType` |
| `"Type1 \| Type2"` | составной тип |
**Ссылочные и объектные (`cfg:Prefix.Name`):**
| DSL | Описание |
|-----|----------|
| `"CatalogRef.XXX"` / `"CatalogObject.XXX"` | Справочник |
| `"DocumentRef.XXX"` / `"DocumentObject.XXX"` | Документ |
| `"EnumRef.XXX"` | Перечисление |
| `"DataProcessorObject.XXX"` / `"ReportObject.XXX"` | Обработка / Отчёт |
| `"InformationRegisterRecordSet.XXX"` | Набор записей регистра сведений |
| `"AccumulationRegisterRecordSet.XXX"` | Набор записей регистра накопления |
| `"DynamicList"` | Динамический список |
Также допустимы: `ChartOfAccountsRef/Object`, `ChartOfCharacteristicTypesRef/Object`, `ChartOfCalculationTypesRef/Object`, `ExchangePlanRef/Object`, `BusinessProcessRef/Object`, `TaskRef/Object`, `AccountingRegisterRecordSet`, `InformationRegisterRecordManager`, `ConstantsSet`.
**Платформенные:**
| DSL | XML |
|-----|-----|
| `"ValueTable"` | `v8:ValueTable` |
| `"ValueTree"` | `v8:ValueTree` |
| `"ValueList"` | `v8:ValueListType` |
| `"TypeDescription"` | `v8:TypeDescription` |
| `"UUID"` | `v8:UUID` |
| `"FormattedString"` | `v8ui:FormattedString` |
| `"Picture"` / `"Color"` / `"Font"` | `v8ui:*` |
| `"DataCompositionSettings"` | `dcsset:DataCompositionSettings` |
| `"Type1 \| Type2"` | составной тип (несколько `<v8:Type>`) |
**Недопустимые типы (XDTO-ошибка при загрузке):**
> `FormDataStructure`, `FormDataCollection`, `FormDataTree` — runtime-типы 1С, не существуют в XML-схеме. Вместо них используйте `CatalogObject.XXX`, `DocumentObject.XXX`, `DataProcessorObject.XXX`, `ValueTable`, `ValueTree`.
## Связки: элемент + реквизит
@@ -309,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": [
@@ -0,0 +1,126 @@
# Form Presets
Пресеты управляют раскладкой форм, генерируемых в режиме `--from-object`.
## Как работает
Цепочка merge (каждый следующий уровень перезаписывает предыдущий через deep merge):
1. **Hardcoded defaults** -- встроены в скрипт, ориентированы на ERP
2. **Built-in preset** -- файл из этой папки (`erp-standard.json` по умолчанию)
3. **Project-level preset** -- файл `presets/skills/form/<name>.json`, поиск вверх от OutputPath
Имя пресета задаётся параметром `--preset` (по умолчанию `erp-standard`).
## Project-level пресет
Чтобы переопределить стандартный пресет в своём проекте, создайте файл:
```
<project-root>/presets/skills/form/erp-standard.json
```
Скрипт ищет этот файл, поднимаясь от OutputPath к корню. Первый найденный файл применяется поверх built-in через deep merge -- не нужно копировать весь пресет, достаточно указать только переопределяемые ключи.
## Секции
Ключи верхнего уровня в JSON -- секции вида `{тип}.{назначение}`:
| Секция | Тип объекта | Назначение формы |
|--------|-------------|------------------|
| `document.item` | Document | Форма документа |
| `document.list` | Document | Форма списка |
| `document.choice` | Document | Форма выбора |
| `catalog.item` | Catalog | Форма элемента |
| `catalog.folder` | Catalog | Форма группы |
| `catalog.list` | Catalog | Форма списка |
| `catalog.choice` | Catalog | Форма выбора |
| `informationRegister.record` | InformationRegister | Форма записи |
| `informationRegister.list` | InformationRegister | Форма списка |
| `accumulationRegister.list` | AccumulationRegister | Форма списка |
| `chartOfCharacteristicTypes.*` | ChartOfCharacteristicTypes | item/folder/list/choice |
| `exchangePlan.*` | ExchangePlan | item/list/choice |
| `chartOfAccounts.*` | ChartOfAccounts | item/folder/list/choice |
### basedOn
Секция может наследовать от другой:
```json
{
"document.choice": {
"basedOn": "document.list",
"properties": { "windowOpeningMode": "LockOwnerWindow" }
}
}
```
## Ключи секций
### Форма объекта (Item/Record)
| Ключ | Описание | Допустимые значения |
|------|----------|---------------------|
| `header.position` | Где размещать шапку | `"insidePage"` -- на первой странице, `"abovePages"` -- над страницами |
| `header.layout` | Колонки шапки | `"1col"`, `"2col"` |
| `header.distribute` | Распределение в 2 колонках | `"even"`, `"left"`, `"right"` |
| `header.dateTitle` | Заголовок даты (Document) | строка, напр. `"от"` |
| `footer.fields` | Поля в подвале | массив имён реквизитов, напр. `["Комментарий"]` |
| `footer.position` | Где размещать подвал | `"insidePage"`, `"belowPages"`, `"none"` |
| `tabularSections.container` | Контейнер табчастей | `"pages"` -- на вкладках, `"inline"` -- в корне, `"single-no-pages"` -- одна ТЧ без страниц |
| `tabularSections.exclude` | Исключить табчасти | массив имён, напр. `["ДополнительныеРеквизиты"]` |
| `tabularSections.lineNumber` | Колонка НомерСтроки | `true` / `false` |
| `additional.position` | Блок доп. реквизитов | `"page"` -- отдельная вкладка, `"below"` -- под табчастями, `"none"` -- не создавать |
| `additional.layout` | Колонки доп. блока | `"1col"`, `"2col"` |
| `additional.bspGroup` | Группа ДополнительныеРеквизиты | `true` / `false` |
| `codeDescription.layout` | Код + Наименование | `"horizontal"`, `"vertical"` |
| `codeDescription.order` | Порядок Код/Наименование | `"descriptionFirst"`, `"codeFirst"` |
| `parent.title` | Заголовок поля Родитель | строка, напр. `"Входит в группу"` |
| `parent.position` | Позиция поля Родитель | `"beforeCodeDescription"`, `"afterCodeDescription"`, `"inHeader"` |
| `owner.readOnly` | Владелец только для чтения | `true` / `false` |
| `owner.position` | Позиция поля Владелец | `"first"` |
| `fieldDefaults.ref.choiceButton` | Кнопка выбора для ссылок | `true` / `false` |
| `fieldDefaults.boolean.element` | Элемент для Boolean | `"check"` (флажок) |
| `commandBar` | Командная панель формы | `"auto"`, `"none"` |
| `properties` | Свойства формы | объект: `autoTitle`, `windowOpeningMode` и др. |
### Форма списка (List/Choice)
| Ключ | Описание | Допустимые значения |
|------|----------|---------------------|
| `columns` | Какие колонки показывать | `"all"` -- все реквизиты, или массив имён |
| `columnType` | Тип элемента колонки | `"labelField"`, `"input"` |
| `hiddenRef` | Скрытая колонка Ref | `true` / `false` |
| `tableCommandBar` | Командная панель таблицы | `"auto"`, `"none"` |
| `commandBar` | Командная панель формы | `"auto"`, `"none"` |
| `choiceMode` | Режим выбора (ChoiceForm) | `true` / `false` |
| `properties` | Свойства формы | объект: `windowOpeningMode` и др. |
## Пример project-level пресета
```json
{
"name": "my-project",
"description": "Стиль форм нашего проекта",
"document.item": {
"header": {
"layout": "1col"
},
"tabularSections": {
"exclude": ["ДополнительныеРеквизиты", "СведенияОСертификатах"]
},
"additional": {
"position": "none"
}
},
"catalog.item": {
"codeDescription": {
"order": "codeFirst"
}
}
}
```
Этот файл переопределяет только указанные ключи -- остальное наследуется из built-in пресета.
@@ -0,0 +1,68 @@
{
"name": "erp-standard",
"description": "ERP 8.3.24 standard form layout",
"document.item": {
"header": {
"position": "insidePage",
"layout": "2col",
"distribute": "even",
"dateTitle": "от"
},
"footer": {
"fields": ["Комментарий"],
"position": "insidePage"
},
"tabularSections": {
"container": "pages",
"exclude": ["ДополнительныеРеквизиты"],
"lineNumber": true
},
"additional": {
"position": "page",
"layout": "2col",
"bspGroup": true
},
"properties": {
"autoTitle": false
}
},
"catalog.item": {
"codeDescription": {
"layout": "horizontal",
"order": "descriptionFirst"
},
"parent": {
"title": "Входит в группу",
"position": "afterCodeDescription"
},
"tabularSections": {
"exclude": ["ДополнительныеРеквизиты", "Представления"]
}
},
"informationRegister.record": {
"properties": {
"windowOpeningMode": "LockOwnerWindow"
}
},
"informationRegister.list": {},
"accumulationRegister.list": {},
"chartOfCharacteristicTypes.item": {
"basedOn": "catalog.item"
},
"exchangePlan.item": {
"basedOn": "catalog.item"
},
"chartOfAccounts.item": {
"parent": {
"title": "Подчинен счету"
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -28
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 формат
@@ -133,33 +133,6 @@ powershell.exe -NoProfile -File .claude/skills/form-edit/scripts/form-edit.ps1 -
Все extension-секции опциональны — без них навык работает как с обычными формами.
## Вывод
```
=== form-edit: Форма ===
[EXTENSION] BaseForm detected — IDs start at 1000000+
Added form events:
+ OnCreateAtServer[After] -> Расш1_ПриСозданииПосле
Added elements (into ГруппаШапка, after Контрагент):
+ [Input] Склад -> Объект.Склад {OnChange}
Added attributes:
+ СуммаИтого: decimal(15,2) (id=1000000)
---
Total: 1 form event(s), 1 element(s) (+2 companions), 1 attribute(s)
Run /form-validate to verify.
```
## Когда использовать
- **После `/form-compile`**: добавить элементы, которые не были в исходном JSON
- **Модификация существующих форм**: добавить поле, реквизит или команду в форму из конфигурации
- **Пакетное добавление**: один JSON может содержать элементы + реквизиты + команды
## Workflow
1. `/form-info` — посмотреть текущую структуру формы
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$FormPath,
[Parameter(Mandatory)]
+15 -5
View File
@@ -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()
@@ -973,8 +973,16 @@ if elements_list:
target_ci = root_ci
if target_ci is None:
# Create ChildItems section in form
target_ci = etree.SubElement(root, f"{{{FORM_NS}}}ChildItems")
# Create ChildItems section in form — insert after Events or AutoCommandBar
target_ci = etree.Element(f"{{{FORM_NS}}}ChildItems")
insert_after = root.find("f:Events", NS)
if insert_after is None:
insert_after = root.find("f:AutoCommandBar", NS)
if insert_after is not None:
idx = list(root).index(insert_after) + 1
root.insert(idx, target_ci)
else:
root.append(target_ci)
root_ci = target_ci
# Detect indent level
@@ -1242,8 +1250,10 @@ if elem_events_list:
# ── 13. Save ────────────────────────────────────────────────
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
# Fix encoding declaration case
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
# Fix XML declaration quotes
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
xml_bytes += b"\n"
# Write with BOM
with open(resolved_form_path, "wb") as f:
f.write(b'\xef\xbb\xbf')
+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>"
```
## Параметры
File diff suppressed because it is too large Load Diff
+71 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-info v1.1 — 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")
@@ -353,7 +353,27 @@ def main():
offset = args.Offset
expand = args.Expand
# --- Validate path ---
# --- Resolve FormPath ---
if not os.path.isabs(form_path):
form_path = os.path.join(os.getcwd(), form_path)
# A: Directory → Ext/Form.xml
if os.path.isdir(form_path):
form_path = os.path.join(form_path, "Ext", "Form.xml")
# B1: Missing Ext/ (Forms/Форма/Form.xml → Forms/Форма/Ext/Form.xml)
if not os.path.isfile(form_path):
fn = os.path.basename(form_path)
if fn == "Form.xml":
c = os.path.join(os.path.dirname(form_path), "Ext", fn)
if os.path.isfile(c):
form_path = c
# B2: Descriptor (Forms/Форма.xml → Forms/Форма/Ext/Form.xml)
if not os.path.isfile(form_path) and form_path.endswith(".xml"):
stem = os.path.splitext(os.path.basename(form_path))[0]
parent = os.path.dirname(form_path)
c = os.path.join(parent, stem, "Ext", "Form.xml")
if os.path.isfile(c):
form_path = c
if not os.path.isfile(form_path):
print(f"File not found: {form_path}", file=sys.stderr)
sys.exit(1)
@@ -464,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)
@@ -474,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
@@ -16,7 +16,9 @@ NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'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)
+3 -24
View File
@@ -17,34 +17,13 @@ allowed-tools:
| Параметр | Обяз. | Умолч. | Описание |
|-----------|:-----:|---------|-----------------------------------------|
| FormPath | да | — | Путь к файлу Form.xml |
| Detailed | нет | — | Показывать [OK] для каждой проверки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
## Команда
```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 | Корневой элемент `<Form>`, version="2.17" | ERROR / WARN |
| 2 | `<AutoCommandBar>` присутствует, id="-1" | ERROR |
| 3 | Уникальность ID элементов (отдельный пул) | ERROR |
| 4 | Уникальность ID реквизитов (отдельный пул) | ERROR |
| 5 | Уникальность ID команд (отдельный пул) | ERROR |
| 6 | Companion-элементы (ContextMenu, ExtendedTooltip, и др.) | ERROR |
| 7 | DataPath → ссылается на существующий реквизит | ERROR |
| 8 | CommandName кнопок → ссылается на существующую команду | ERROR |
| 9 | События имеют непустые имена обработчиков | ERROR |
| 10 | Команды имеют Action (обработчик) | ERROR |
| 11 | Не более одного MainAttribute | ERROR |
| 12 | BaseForm: наличие и version (при расширении) | OK / WARN |
| 13 | callType значения: Before, After, Override | ERROR |
| 14 | ID расширения >= 1000000 для добавленных attrs/commands | WARN |
| 15 | callType без BaseForm — некорректная структура | WARN |
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
@@ -1,7 +1,8 @@
# form-validate v1.1 — Validate 1C managed form
# form-validate v1.4 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$FormPath,
[switch]$Detailed,
@@ -58,6 +59,20 @@ $nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
$root = $xmlDoc.DocumentElement
# --- Detect context: config vs EPF/ERF ---
# Walk up from FormPath looking for Configuration.xml → config context
# No Configuration.xml → external data processor / report (EPF/ERF)
$script:isConfigContext = $false
$walkDir = Split-Path (Resolve-Path $FormPath) -Parent
for ($i = 0; $i -lt 15; $i++) {
if (-not $walkDir -or $walkDir -eq (Split-Path $walkDir)) { break }
if (Test-Path (Join-Path $walkDir "Configuration.xml")) {
$script:isConfigContext = $true
break
}
$walkDir = Split-Path $walkDir
}
# --- Counters ---
$errors = 0
@@ -112,10 +127,10 @@ if ($root.LocalName -ne "Form") {
Report-Error "Root element is '$($root.LocalName)', expected 'Form'"
} else {
$version = $root.GetAttribute("version")
if ($version -eq "2.17") {
if ($version -eq "2.17" -or $version -eq "2.20") {
Report-OK "Root element: Form version=$version"
} elseif ($version) {
Report-Warn "Form version='$version' (expected 2.17)"
Report-Warn "Form version='$version' (expected 2.17 or 2.20)"
} else {
Report-Warn "Form version attribute missing"
}
@@ -667,6 +682,84 @@ if (-not $stopped -and -not $isExtension) {
}
}
# --- Check 12: Type values validation ---
$knownInvalidTypes = @(
"FormDataStructure","FormDataCollection","FormDataTree","FormDataTreeItem","FormDataCollectionItem"
"FormGroup","FormField","FormButton","FormDecoration","FormTable"
)
$validClosedTypes = @(
"xs:boolean","xs:string","xs:decimal","xs:dateTime","xs:binary"
"v8:FillChecking","v8:Null","v8:StandardPeriod","v8:StandardBeginningDate","v8:Type"
"v8:TypeDescription","v8:UUID","v8:ValueListType","v8:ValueTable","v8:ValueTree"
"v8:Universal","v8:FixedArray","v8:FixedStructure"
"v8ui:Color","v8ui:Font","v8ui:FormattedString","v8ui:HorizontalAlign"
"v8ui:Picture","v8ui:SizeChangeMode","v8ui:VerticalAlign"
"dcsset:DataCompositionComparisonType","dcsset:DataCompositionFieldPlacement"
"dcsset:Filter","dcsset:SettingsComposer","dcsset:DataCompositionSettings"
"dcssch:DataCompositionSchema"
"dcscor:DataCompositionComparisonType","dcscor:DataCompositionGroupType"
"dcscor:DataCompositionPeriodAdditionType","dcscor:DataCompositionSortDirection","dcscor:Field"
"ent:AccountType","ent:AccumulationRecordType","ent:AccountingRecordType"
)
$validCfgPrefixes = @(
"AccountingRegisterRecordSet","AccumulationRegisterRecordSet"
"BusinessProcessObject","BusinessProcessRef"
"CatalogObject","CatalogRef"
"ChartOfAccountsObject","ChartOfAccountsRef"
"ChartOfCalculationTypesObject","ChartOfCalculationTypesRef"
"ChartOfCharacteristicTypesObject","ChartOfCharacteristicTypesRef"
"ConstantsSet","DataProcessorObject","DocumentObject","DocumentRef"
"DynamicList","EnumRef","ExchangePlanObject","ExchangePlanRef"
"ExternalDataProcessorObject","ExternalReportObject"
"InformationRegisterRecordManager","InformationRegisterRecordSet"
"ReportObject","TaskObject","TaskRef"
)
if (-not $stopped) {
$typeNodes = $root.SelectNodes("//v8:Type", $nsMgr)
$typeOk = $true
$typeChecked = 0
$typeInvalid = 0
foreach ($tn in $typeNodes) {
$tv = $tn.InnerText.Trim()
if (-not $tv) { continue }
$typeChecked++
if ($tv -in $knownInvalidTypes) {
Report-Error "12. Type '$tv': invalid runtime/UI type (not valid in XDTO schema)"
$typeOk = $false; $typeInvalid++
continue
}
if ($tv -in $validClosedTypes) { continue }
if ($tv -match '^cfg:(.+)$') {
$cfgVal = $Matches[1]
if ($cfgVal -eq "DynamicList") { continue }
if ($cfgVal -match '^([^.]+)\.') {
$pfx = $Matches[1]
if ($pfx -in $validCfgPrefixes) {
# ExternalDataProcessorObject/ExternalReportObject valid only for EPF/ERF, not config
if ($script:isConfigContext -and ($pfx -eq "ExternalDataProcessorObject" -or $pfx -eq "ExternalReportObject")) {
Report-Error "12. Type '$tv': External* type in configuration context (use DataProcessorObject/ReportObject instead)"
$typeOk = $false; $typeInvalid++
}
continue
}
}
Report-Warn "12. Type '$tv': unrecognized cfg prefix"
$typeOk = $false
continue
}
if ($tv -match ':') { continue }
Report-Warn "12. Type '$tv': bare type without namespace prefix"
$typeOk = $false
}
if ($typeChecked -eq 0) {
Report-OK "12. Types: no type values to check"
} elseif ($typeOk) {
Report-OK "12. Types: $typeChecked values, all valid"
}
}
# --- Summary ---
$checks = $script:okCount + $errors + $warnings
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-validate v1.1 — Validate 1C managed form
# form-validate v1.4 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -13,6 +13,41 @@ V8_NS = "http://v8.1c.ru/8.1/data/core"
NSMAP = {"f": F_NS, "v8": V8_NS}
KNOWN_INVALID_TYPES = {
'FormDataStructure', 'FormDataCollection', 'FormDataTree',
'FormDataTreeItem', 'FormDataCollectionItem',
'FormGroup', 'FormField', 'FormButton', 'FormDecoration', 'FormTable',
}
VALID_CLOSED_TYPES = {
'xs:boolean', 'xs:string', 'xs:decimal', 'xs:dateTime', 'xs:binary',
'v8:FillChecking', 'v8:Null', 'v8:StandardPeriod', 'v8:StandardBeginningDate', 'v8:Type',
'v8:TypeDescription', 'v8:UUID', 'v8:ValueListType', 'v8:ValueTable', 'v8:ValueTree',
'v8:Universal', 'v8:FixedArray', 'v8:FixedStructure',
'v8ui:Color', 'v8ui:Font', 'v8ui:FormattedString', 'v8ui:HorizontalAlign',
'v8ui:Picture', 'v8ui:SizeChangeMode', 'v8ui:VerticalAlign',
'dcsset:DataCompositionComparisonType', 'dcsset:DataCompositionFieldPlacement',
'dcsset:Filter', 'dcsset:SettingsComposer', 'dcsset:DataCompositionSettings',
'dcssch:DataCompositionSchema',
'dcscor:DataCompositionComparisonType', 'dcscor:DataCompositionGroupType',
'dcscor:DataCompositionPeriodAdditionType', 'dcscor:DataCompositionSortDirection', 'dcscor:Field',
'ent:AccountType', 'ent:AccumulationRecordType', 'ent:AccountingRecordType',
}
VALID_CFG_PREFIXES = {
'AccountingRegisterRecordSet', 'AccumulationRegisterRecordSet',
'BusinessProcessObject', 'BusinessProcessRef',
'CatalogObject', 'CatalogRef',
'ChartOfAccountsObject', 'ChartOfAccountsRef',
'ChartOfCalculationTypesObject', 'ChartOfCalculationTypesRef',
'ChartOfCharacteristicTypesObject', 'ChartOfCharacteristicTypesRef',
'ConstantsSet', 'DataProcessorObject', 'DocumentObject', 'DocumentRef',
'DynamicList', 'EnumRef', 'ExchangePlanObject', 'ExchangePlanRef',
'ExternalDataProcessorObject', 'ExternalReportObject',
'InformationRegisterRecordManager', 'InformationRegisterRecordSet',
'ReportObject', 'TaskObject', 'TaskRef',
}
def localname(el):
return etree.QName(el.tag).localname
@@ -22,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()
@@ -69,6 +104,18 @@ def main():
root = tree.getroot()
# Detect context: config vs EPF/ERF
is_config_context = False
walk_dir = os.path.dirname(os.path.abspath(form_path))
for _ in range(15):
parent = os.path.dirname(walk_dir)
if parent == walk_dir:
break
if os.path.isfile(os.path.join(walk_dir, 'Configuration.xml')):
is_config_context = True
break
walk_dir = parent
errors = 0
warnings = 0
ok_count = 0
@@ -114,10 +161,10 @@ def main():
report_error(f"Root element is '{localname(root)}', expected 'Form'")
else:
version = root.get("version", "")
if version == "2.17":
if version in ("2.17", "2.20"):
report_ok(f"Root element: Form version={version}")
elif version:
report_warn(f"Form version='{version}' (expected 2.17)")
report_warn(f"Form version='{version}' (expected 2.17 or 2.20)")
else:
report_warn("Form version attribute missing")
@@ -588,6 +635,48 @@ def main():
if call_type_without_base:
report_warn("callType attributes found but no BaseForm \u2014 possible incorrect structure")
# --- Check 12: Type validation ---
if not stopped:
type_nodes = root.xpath('//v8:Type', namespaces={'v8': V8_NS})
type_error_count = 0
type_warn_count = 0
type_count = len(type_nodes)
for tn in type_nodes:
if stopped:
break
tv = (tn.text or "").strip()
if not tv:
continue
if tv in KNOWN_INVALID_TYPES:
report_error(f'12. Type "{tv}": invalid runtime/UI type (not valid in XDTO schema)')
type_error_count += 1
elif tv in VALID_CLOSED_TYPES:
pass # OK
elif tv.startswith("cfg:"):
suffix = tv[4:] # after "cfg:"
prefix = suffix.split(".")[0]
if prefix in VALID_CFG_PREFIXES or suffix == "DynamicList":
# ExternalDataProcessorObject/ExternalReportObject valid only in EPF/ERF context
if is_config_context and prefix in ('ExternalDataProcessorObject', 'ExternalReportObject'):
report_error(f'12. Type "{tv}": External* type in configuration context (use DataProcessorObject/ReportObject instead)')
type_invalid += 1
else:
report_warn(f'12. Type "{tv}": unrecognized cfg prefix')
type_warn_count += 1
elif ":" in tv:
pass # unknown namespace, pass through
else:
report_warn(f'12. Type "{tv}": bare type without namespace prefix')
type_warn_count += 1
if type_error_count == 0 and type_warn_count == 0:
if type_count > 0:
report_ok(f'12. Types: {type_count} values, all valid')
else:
report_ok('12. Types: no type values to check')
# --- Finalize ---
checks = ok_count + errors + warnings
if errors == 0 and warnings == 0 and not detailed:
+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>"]
```
## Что делает скрипт
+23 -2
View File
@@ -1,4 +1,4 @@
# help-add v1.2 — 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,27 @@ param(
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- 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
# --- Проверки ---
@@ -35,7 +56,7 @@ $encBom = New-Object System.Text.UTF8Encoding($true)
$helpXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
<Page>$Lang</Page>
</Help>
"@
+24 -3
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
# add-help v1.2 — 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
import os
import re
import sys
from lxml import etree
@@ -11,10 +12,28 @@ 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"encoding='UTF-8'", b'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)
@@ -39,6 +58,8 @@ def main():
lang = args.Lang
src_dir = args.SrcDir
format_version = detect_format_version(os.path.abspath(src_dir))
# --- Checks ---
object_dir = os.path.join(src_dir, object_name)
@@ -60,7 +81,7 @@ def main():
'<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f' version="{format_version}">\n'
f'\t<Page>{lang}</Page>\n'
'</Help>'
)
+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`).
+59 -2
View File
@@ -11,8 +11,65 @@ allowed-tools:
# /interface-edit — редактирование CommandInterface.xml
Операции: hide, show, place, order, subsystem-order, group-order. Подробнее: `.claude/skills/interface-edit/reference.md`
Точечное редактирование файла командного интерфейса подсистемы 1С.
## Параметры
| Параметр | Обяз. | Описание |
|----------|:-----:|----------|
| CIPath | да | Путь к CommandInterface.xml |
| Operation | нет | Операция: hide, show, place, order, subsystem-order, group-order |
| Value | нет | Значение для операции |
| DefinitionFile | нет | JSON-файл с массивом операций (альтернатива Operation) |
| CreateIfMissing | нет | Создать файл если не существует |
| NoValidate | нет | Пропустить авто-валидацию |
## Команда
### 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_SKILL_DIR}/scripts/interface-edit.ps1" -CIPath '<path>' -DefinitionFile '<json>'
```
## Операции
| Операция | Значение | Описание |
|----------|----------|----------|
| hide | Cmd.Name или массив | Скрыть команду (CommandsVisibility, false) |
| show | Cmd.Name или массив | Показать команду (visibility, true) |
| place | {"command":"...","group":"CommandGroup.X"} | Разместить команду в группе |
| order | {"group":"...","commands":[...]} | Задать порядок команд в группе |
| subsystem-order | ["Subsystem.X.Subsystem.A",...] | Порядок дочерних подсистем |
| group-order | ["NavigationPanelOrdinary",...] | Порядок групп |
## Примеры
```powershell
# Скрыть команду
... -CIPath Subsystems/Продажи/Ext/CommandInterface.xml -Operation hide -Value "Catalog.Товары.StandardCommand.OpenList"
# Показать команду
... -Operation show -Value "Report.Продажи.Command.Отчёт"
# Разместить в группе
... -Operation place -Value '{"command":"Report.X.Command.Y","group":"CommandGroup.Отчеты"}'
# Задать порядок подсистем
... -Operation subsystem-order -Value '["Subsystem.X.Subsystem.A","Subsystem.X.Subsystem.B"]'
# Создать новый CI
... -CIPath <new-path> -Operation subsystem-order -Value '[...]' -CreateIfMissing
```
## Верификация
```
/interface-validate <CIPath>
```
@@ -1,48 +0,0 @@
# /interface-edit — редактирование CommandInterface.xml
Точечное редактирование файла командного интерфейса подсистемы 1С.
## Параметры
| Параметр | Описание |
|----------|----------|
| CIPath | Путь к CommandInterface.xml |
| DefinitionFile | JSON-файл с массивом операций |
| Operation | Одна операция: hide, show, place, order, subsystem-order, group-order |
| Value | Значение для операции |
| CreateIfMissing | Создать файл если не существует |
| NoValidate | Пропустить авто-валидацию |
## Операции
| Операция | Значение | Описание |
|----------|----------|----------|
| hide | Cmd.Name или массив | Скрыть команду (CommandsVisibility, false) |
| show | Cmd.Name или массив | Показать команду (visibility, true) |
| place | {"command":"...","group":"CommandGroup.X"} | Разместить команду в группе |
| order | {"group":"...","commands":[...]} | Задать порядок команд в группе |
| subsystem-order | ["Subsystem.X.Subsystem.A",...] | Порядок дочерних подсистем |
| group-order | ["NavigationPanelOrdinary",...] | Порядок групп |
## Примеры
```powershell
# Скрыть команду
... -CIPath Subsystems/Продажи/Ext/CommandInterface.xml -Operation hide -Value "Catalog.Товары.StandardCommand.OpenList"
# Показать команду
... -Operation show -Value "Report.Продажи.Command.Отчёт"
# Разместить в группе
... -Operation place -Value '{"command":"Report.X.Command.Y","group":"CommandGroup.Отчеты"}'
# Задать порядок подсистем
... -Operation subsystem-order -Value '["Subsystem.X.Subsystem.A","Subsystem.X.Subsystem.B"]'
# Создать новый CI
... -CIPath <new-path> -Operation subsystem-order -Value '[...]' -CreateIfMissing
```
## Авто-валидация
После каждой операции автоматически запускается `/interface-validate`. Подавить: `-NoValidate`.
@@ -1,7 +1,7 @@
# interface-edit v1.0 — Edit 1C CommandInterface.xml
# 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,
@@ -23,6 +23,25 @@ if (-not [System.IO.Path]::IsPathRooted($CIPath)) {
}
$resolvedPath = $CIPath
# --- 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 ([System.IO.Path]::GetDirectoryName($CIPath))
# --- Namespaces ---
$script:ciNs = "http://v8.1c.ru/8.3/xcf/extrnprops"
$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable"
@@ -42,7 +61,7 @@ if (-not (Test-Path $CIPath)) {
xmlns:xr="$($script:xrNs)"
xmlns:xs="$($script:xsNs)"
xmlns:xsi="$($script:xsiNs)"
version="2.17">
version="$formatVersion">
</CommandInterface>
"@
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
@@ -202,9 +221,59 @@ function Find-CommandByName($section, [string]$cmdName) {
return $null
}
# --- Command name normalization (plural/Russian type prefix → singular English) ---
$script:typeNormMap = @{
"Catalogs"="Catalog"; "Documents"="Document"; "Enums"="Enum"; "Constants"="Constant"
"Reports"="Report"; "DataProcessors"="DataProcessor"
"InformationRegisters"="InformationRegister"; "AccumulationRegisters"="AccumulationRegister"
"AccountingRegisters"="AccountingRegister"; "CalculationRegisters"="CalculationRegister"
"ChartsOfAccounts"="ChartOfAccounts"; "ChartsOfCharacteristicTypes"="ChartOfCharacteristicTypes"
"ChartsOfCalculationTypes"="ChartOfCalculationTypes"
"BusinessProcesses"="BusinessProcess"; "Tasks"="Task"
"ExchangePlans"="ExchangePlan"; "DocumentJournals"="DocumentJournal"
"CommonModules"="CommonModule"; "CommonCommands"="CommonCommand"
"CommonForms"="CommonForm"; "CommonPictures"="CommonPicture"
"CommonTemplates"="CommonTemplate"; "CommonAttributes"="CommonAttribute"
"CommandGroups"="CommandGroup"; "Roles"="Role"
"Subsystems"="Subsystem"; "StyleItems"="StyleItem"
# Russian singular
"Справочник"="Catalog"; "Документ"="Document"; "Перечисление"="Enum"
"Константа"="Constant"; "Отчёт"="Report"; "Отчет"="Report"; "Обработка"="DataProcessor"
"РегистрСведений"="InformationRegister"; "РегистрНакопления"="AccumulationRegister"
"РегистрБухгалтерии"="AccountingRegister"
"ПланСчетов"="ChartOfAccounts"; "ПланВидовХарактеристик"="ChartOfCharacteristicTypes"
"БизнесПроцесс"="BusinessProcess"; "Задача"="Task"
"ПланОбмена"="ExchangePlan"; "ЖурналДокументов"="DocumentJournal"
"ОбщийМодуль"="CommonModule"; "ОбщаяКоманда"="CommonCommand"
"ОбщаяФорма"="CommonForm"; "Подсистема"="Subsystem"
# Russian plural
"Справочники"="Catalog"; "Документы"="Document"; "Перечисления"="Enum"
"Константы"="Constant"; "Отчёты"="Report"; "Отчеты"="Report"; "Обработки"="DataProcessor"
"РегистрыСведений"="InformationRegister"; "РегистрыНакопления"="AccumulationRegister"
"РегистрыБухгалтерии"="AccountingRegister"
"ПланыСчетов"="ChartOfAccounts"; "ПланыВидовХарактеристик"="ChartOfCharacteristicTypes"
"БизнесПроцессы"="BusinessProcess"; "Задачи"="Task"
"ПланыОбмена"="ExchangePlan"; "ЖурналыДокументов"="DocumentJournal"
"Подсистемы"="Subsystem"
}
function Normalize-CmdName([string]$name) {
if (-not $name -or -not $name.Contains('.')) { return $name }
$dotIdx = $name.IndexOf('.')
$first = $name.Substring(0, $dotIdx)
$rest = $name.Substring($dotIdx)
if ($script:typeNormMap.ContainsKey($first)) {
$normalized = "$($script:typeNormMap[$first])$rest"
if ($normalized -ne $name) { Write-Host "[NORM] Command: $name -> $normalized" }
return $normalized
}
return $name
}
# --- Operations ---
function Do-Hide([string[]]$commands) {
$commands = @($commands | ForEach-Object { Normalize-CmdName $_ })
$section = Ensure-Section "CommandsVisibility"
$sectionIndent = Get-ChildIndent $section
@@ -244,6 +313,7 @@ function Do-Hide([string[]]$commands) {
}
function Do-Show([string[]]$commands) {
$commands = @($commands | ForEach-Object { Normalize-CmdName $_ })
$section = $null
foreach ($child in $root.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "CommandsVisibility") {
@@ -292,7 +362,7 @@ function Do-Show([string[]]$commands) {
function Do-Place([string]$jsonVal) {
$def = $jsonVal | ConvertFrom-Json
$cmdName = "$($def.command)"
$cmdName = Normalize-CmdName "$($def.command)"
$groupName = "$($def.group)"
if (-not $cmdName -or -not $groupName) { Write-Error "place requires {command, group}"; exit 1 }
@@ -326,7 +396,7 @@ function Do-Place([string]$jsonVal) {
function Do-Order([string]$jsonVal) {
$def = $jsonVal | ConvertFrom-Json
$groupName = "$($def.group)"
$commands = @($def.commands | ForEach-Object { "$_" })
$commands = @($def.commands | ForEach-Object { Normalize-CmdName "$_" })
if (-not $groupName -or $commands.Count -eq 0) { Write-Error "order requires {group, commands:[...]}"; exit 1 }
$section = Ensure-Section "CommandsOrder"
@@ -436,7 +506,9 @@ if ($DefinitionFile) {
foreach ($op in $operations) {
$opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" }
$opValue = if ($op.value) { "$($op.value)" } else { "$Value" }
$opValueRaw = if ($op.value) { $op.value } else { "$Value" }
# For operations expecting JSON (place, order, etc.): accept object or string
$opValue = if ($opValueRaw -is [string]) { $opValueRaw } else { $opValueRaw | ConvertTo-Json -Compress }
switch ($opName) {
"hide" { Do-Hide (Parse-ValueList $opValue) }
@@ -1,14 +1,31 @@
#!/usr/bin/env python3
# interface-edit v1.0 — Edit 1C CommandInterface.xml
# interface-edit v1.3 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import subprocess
import sys
from lxml import etree
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"
CI_NS = "http://v8.1c.ru/8.3/xcf/extrnprops"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
@@ -95,12 +112,64 @@ def parse_value_list(val):
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'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)
TYPE_NORM_MAP = {
'Catalogs': 'Catalog', 'Documents': 'Document', 'Enums': 'Enum',
'Constants': 'Constant', 'Reports': 'Report', 'DataProcessors': 'DataProcessor',
'InformationRegisters': 'InformationRegister', 'AccumulationRegisters': 'AccumulationRegister',
'AccountingRegisters': 'AccountingRegister', 'CalculationRegisters': 'CalculationRegister',
'ChartsOfAccounts': 'ChartOfAccounts', 'ChartsOfCharacteristicTypes': 'ChartOfCharacteristicTypes',
'ChartsOfCalculationTypes': 'ChartOfCalculationTypes',
'BusinessProcesses': 'BusinessProcess', 'Tasks': 'Task',
'ExchangePlans': 'ExchangePlan', 'DocumentJournals': 'DocumentJournal',
'CommonModules': 'CommonModule', 'CommonCommands': 'CommonCommand',
'CommonForms': 'CommonForm', 'CommonPictures': 'CommonPicture',
'CommonTemplates': 'CommonTemplate', 'CommonAttributes': 'CommonAttribute',
'CommandGroups': 'CommandGroup', 'Roles': 'Role',
'Subsystems': 'Subsystem', 'StyleItems': 'StyleItem',
# Russian singular
'Справочник': 'Catalog', 'Документ': 'Document', 'Перечисление': 'Enum',
'Константа': 'Constant', 'Отчёт': 'Report', 'Отчет': 'Report', 'Обработка': 'DataProcessor',
'РегистрСведений': 'InformationRegister', 'РегистрНакопления': 'AccumulationRegister',
'РегистрБухгалтерии': 'AccountingRegister',
'ПланСчетов': 'ChartOfAccounts', 'ПланВидовХарактеристик': 'ChartOfCharacteristicTypes',
'БизнесПроцесс': 'BusinessProcess', 'Задача': 'Task',
'ПланОбмена': 'ExchangePlan', 'ЖурналДокументов': 'DocumentJournal',
'ОбщийМодуль': 'CommonModule', 'ОбщаяКоманда': 'CommonCommand',
'ОбщаяФорма': 'CommonForm', 'Подсистема': 'Subsystem',
# Russian plural
'Справочники': 'Catalog', 'Документы': 'Document', 'Перечисления': 'Enum',
'Константы': 'Constant', 'Отчёты': 'Report', 'Отчеты': 'Report', 'Обработки': 'DataProcessor',
'РегистрыСведений': 'InformationRegister', 'РегистрыНакопления': 'AccumulationRegister',
'РегистрыБухгалтерии': 'AccountingRegister',
'ПланыСчетов': 'ChartOfAccounts', 'ПланыВидовХарактеристик': 'ChartOfCharacteristicTypes',
'БизнесПроцессы': 'BusinessProcess', 'Задачи': 'Task',
'ПланыОбмена': 'ExchangePlan', 'ЖурналыДокументов': 'DocumentJournal',
'Подсистемы': 'Subsystem',
}
def normalize_cmd_name(name):
if not name or '.' not in name:
return name
dot_idx = name.index('.')
first = name[:dot_idx]
rest = name[dot_idx:]
if first in TYPE_NORM_MAP:
normalized = TYPE_NORM_MAP[first] + rest
if normalized != name:
print(f'[NORM] Command: {name} -> {normalized}')
return normalized
return name
def find_command_by_name(section, cmd_name):
for child in section:
if isinstance(child.tag, str) and localname(child) == "Command":
@@ -113,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)
@@ -129,6 +198,10 @@ def main():
print("Either -DefinitionFile or -Operation is required", file=sys.stderr)
sys.exit(1)
# --- Detect format version ---
ci_dir = os.path.dirname(os.path.abspath(args.CIPath))
format_version = detect_format_version(ci_dir)
# --- Resolve path ---
ci_path = args.CIPath
if not os.path.isabs(ci_path):
@@ -147,7 +220,7 @@ def main():
f'\txmlns:xr="{XR_NS}"\n'
f'\txmlns:xs="{XS_NS}"\n'
f'\txmlns:xsi="{XSI_NS}"\n'
f'\tversion="2.17">\n'
f'\tversion="{format_version}">\n'
f'</CommandInterface>'
)
with open(ci_path, "w", encoding="utf-8-sig") as fh:
@@ -205,6 +278,7 @@ def main():
def do_hide(commands):
nonlocal add_count, modify_count
commands = [normalize_cmd_name(c) for c in commands]
section = ensure_section("CommandsVisibility")
section_indent = get_child_indent(section)
@@ -236,6 +310,7 @@ def main():
def do_show(commands):
nonlocal add_count, modify_count
commands = [normalize_cmd_name(c) for c in commands]
section = None
for child in root:
if isinstance(child.tag, str) and localname(child) == "CommandsVisibility":
@@ -274,8 +349,8 @@ def main():
def do_place(json_val):
nonlocal add_count, modify_count
defn = json.loads(json_val)
cmd_name = str(defn["command"])
defn = json_val if isinstance(json_val, dict) else json.loads(json_val)
cmd_name = normalize_cmd_name(str(defn["command"]))
group_name = str(defn["group"])
if not cmd_name or not group_name:
print("place requires {command, group}", file=sys.stderr)
@@ -302,9 +377,9 @@ def main():
def do_order(json_val):
nonlocal add_count, remove_count
defn = json.loads(json_val)
defn = json_val if isinstance(json_val, dict) else json.loads(json_val)
group_name = str(defn["group"])
commands = [str(c) for c in defn["commands"]]
commands = [normalize_cmd_name(str(c)) for c in defn["commands"]]
if not group_name or not commands:
print("order requires {group, commands:[...]}", file=sys.stderr)
sys.exit(1)
@@ -336,7 +411,7 @@ def main():
def do_subsystem_order(json_val):
nonlocal add_count, remove_count
parsed = json.loads(json_val)
parsed = json_val if isinstance(json_val, list) else json.loads(json_val)
subsystems = [str(s) for s in parsed]
if not subsystems:
print("subsystem-order requires array of subsystem paths", file=sys.stderr)
@@ -361,7 +436,7 @@ def main():
def do_group_order(json_val):
nonlocal add_count, remove_count
parsed = json.loads(json_val)
parsed = json_val if isinstance(json_val, list) else json.loads(json_val)
groups = [str(g) for g in parsed]
if not groups:
print("group-order requires array of group names", file=sys.stderr)
@@ -429,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()
+3 -23
View File
@@ -17,33 +17,13 @@ allowed-tools:
| Параметр | Обяз. | Умолч. | Описание |
|-----------|:-----:|---------|-----------------------------------------|
| CIPath | да | — | Путь к CommandInterface.xml |
| Detailed | нет | — | Показывать [OK] для каждой проверки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда
```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"
```
## Проверки (13)
| # | Проверка | Серьёзность |
|----|--------------------------------------------------------------|-------------|
| 1 | XML well-formedness + root element (CommandInterface, version, namespace) | ERROR |
| 2 | Допустимые дочерние элементы (только 5 секций) | ERROR |
| 3 | Порядок секций корректен | ERROR |
| 4 | Нет дублирующихся секций | ERROR |
| 5 | CommandsVisibility — Command.name + Visibility/xr:Common | ERROR |
| 6 | CommandsVisibility — нет дубликатов по name | WARN |
| 7 | CommandsPlacement — Command.name + CommandGroup + Placement | ERROR |
| 8 | CommandsOrder — Command.name + CommandGroup | ERROR |
| 9 | SubsystemsOrder — Subsystem непустой, формат Subsystem.X | ERROR |
| 10 | SubsystemsOrder — нет дубликатов | WARN |
| 11 | GroupsOrder — Group непустой | ERROR |
| 12 | GroupsOrder — нет дубликатов | WARN |
| 13 | Формат ссылок на команды | WARN |
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
@@ -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 -8
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>"
```
| Параметр | Описание |
@@ -117,9 +117,3 @@ powershell.exe -NoProfile -File .claude/skills/meta-compile/scripts/meta-compile
]
```
## Что генерируется
- `{TypePlural}/{Name}.xml` — метаданные объекта
- `{TypePlural}/{Name}/Ext/*.bsl` — модули (ObjectModule, RecordSetModule, Module — зависит от типа)
- `Configuration.xml` — автоматическая регистрация в `<ChildObjects>`
@@ -6,13 +6,20 @@
|-----------|----------|-------------|
| `hierarchical` | `false` | Hierarchical |
| `hierarchyType` | `HierarchyFoldersAndItems` | HierarchyType |
| `limitLevelCount` | `false` | LimitLevelCount |
| `levelCount` | `2` | LevelCount |
| `foldersOnTop` | `true` | FoldersOnTop |
| `codeLength` | `9` | CodeLength |
| `codeType` | `String` | CodeType |
| `codeAllowedLength` | `Variable` | CodeAllowedLength |
| `codeSeries` | `WholeCatalog` | CodeSeries |
| `descriptionLength` | `25` | DescriptionLength |
| `autonumbering` | `true` | Autonumbering |
| `checkUnique` | `false` | CheckUnique |
| `defaultPresentation` | `AsDescription` | DefaultPresentation |
| `subordinationUse` | `ToItems` | SubordinationUse |
| `quickChoice` | `false` | QuickChoice |
| `choiceMode` | `BothWays` | ChoiceMode |
| `owners` | `[]` | Owners |
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
@@ -156,15 +156,15 @@
| `descriptionLength` | `25` | DescriptionLength |
| `autonumbering` | `true` | Autonumbering |
| `checkUnique` | `false` | CheckUnique |
| `dependenceOnCalculationTypes` | `NotUsed` | DependenceOnCalculationTypes |
| `dependenceOnCalculationTypes` | `DontUse` | DependenceOnCalculationTypes |
| `actionPeriodUse` | `false` | ActionPeriodUse |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
`dependenceOnCalculationTypes`: `NotUsed`, `ExclusionAndDependence`, `ExclusionOnly`.
`dependenceOnCalculationTypes`: `DontUse`, `OnActionPeriod`.
```json
{ "type": "ChartOfCalculationTypes", "name": "Начисления", "dependenceOnCalculationTypes": "ExclusionAndDependence" }
{ "type": "ChartOfCalculationTypes", "name": "Начисления", "dependenceOnCalculationTypes": "OnActionPeriod" }
```
## Зависимости
@@ -1,4 +1,4 @@
# meta-compile v1.3 — Compile 1C metadata object from JSON
# meta-compile v1.11 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -79,6 +79,96 @@ $script:objectTypeSynonyms = @{
"ОпределяемыйТип" = "DefinedType"
}
# Enum property value synonyms — model often gets these slightly wrong
$script:enumValueAliases = @{
# RegisterType (AccumulationRegister)
"Balances" = "Balance"; "Остатки" = "Balance"; "Обороты" = "Turnovers"
# WriteMode (InformationRegister)
"RecordSubordinate" = "RecorderSubordinate"; "Subordinate" = "RecorderSubordinate"
"ПодчинениеРегистратору" = "RecorderSubordinate"; "Независимый" = "Independent"
# DependenceOnCalculationTypes (ChartOfCalculationTypes)
"NotDependOnCalculationTypes" = "DontUse"; "NoDependence" = "DontUse"; "NotUsed" = "DontUse"
"Depend" = "OnActionPeriod"; "ПоПериодуДействия" = "OnActionPeriod"
# InformationRegisterPeriodicity
"None" = "Nonperiodical"; "Daily" = "Day"; "Monthly" = "Month"
"Quarterly" = "Quarter"; "Yearly" = "Year"
"Непериодический" = "Nonperiodical"; "Секунда" = "Second"; "День" = "Day"
"Месяц" = "Month"; "Квартал" = "Quarter"; "Год" = "Year"
"ПозицияРегистратора" = "RecorderPosition"
# DataLockControlMode
"Автоматический" = "Automatic"; "Управляемый" = "Managed"
# FullTextSearch
"Использовать" = "Use"; "НеИспользовать" = "DontUse"
# Posting
"Разрешить" = "Allow"; "Запретить" = "Deny"
# EditType
"ВДиалоге" = "InDialog"; "ВСписке" = "InList"; "ОбаСпособа" = "BothWays"
# DefaultPresentation
"ВВидеНаименования" = "AsDescription"; "ВВидеКода" = "AsCode"
# FillChecking
"НеПроверять" = "DontCheck"; "Ошибка" = "ShowError"; "Предупреждение" = "ShowWarning"
# Indexing
"НеИндексировать" = "DontIndex"; "Индексировать" = "Index"
"ИндексироватьСДопУпорядочиванием" = "IndexWithAdditionalOrder"
}
# Valid enum values per property (from meta-validate)
$script:validEnumValues = @{
"RegisterType" = @("Balance","Turnovers")
"WriteMode" = @("Independent","RecorderSubordinate")
"InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition")
"DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod")
"DataLockControlMode" = @("Automatic","Managed")
"FullTextSearch" = @("Use","DontUse")
"DataHistory" = @("Use","DontUse")
"DefaultPresentation" = @("AsDescription","AsCode")
"Posting" = @("Allow","Deny")
"RealTimePosting" = @("Allow","Deny")
"EditType" = @("InDialog","InList","BothWays")
"HierarchyType" = @("HierarchyFoldersAndItems","HierarchyItemsOnly")
"CodeType" = @("String","Number")
"CodeAllowedLength" = @("Variable","Fixed")
"NumberType" = @("String","Number")
"NumberAllowedLength" = @("Variable","Fixed")
"RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff")
"RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll")
"ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession")
"ReuseSessions" = @("DontUse","AutoUse")
"FillChecking" = @("DontCheck","ShowError","ShowWarning")
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
"SubordinationUse" = @("ToItems","ToFolders","ToFoldersAndItems")
"CodeSeries" = @("WholeCatalog","WithinSubordination")
"ChoiceMode" = @("BothWays","QuickChoice","FromForm")
}
function Normalize-EnumValue {
param([string]$propName, [string]$value)
# 1. Check alias dictionary — silent auto-correct
if ($script:enumValueAliases.ContainsKey($value)) {
return $script:enumValueAliases[$value]
}
# 2. Case-insensitive match against valid values — silent
$valid = $script:validEnumValues[$propName]
if ($valid) {
foreach ($v in $valid) {
if ($v -ieq $value) { return $v }
}
# 3. Known property, unknown value — error with hint
Write-Error "Invalid value '$value' for property '$propName'. Valid values: $($valid -join ', ')"
exit 1
}
# 4. Unknown property — pass-through (no validation data)
return $value
}
# Helper: read enum property from $def with default and normalization
function Get-EnumProp {
param([string]$propName, [string]$fieldName, [string]$default)
$val = $def.$fieldName
$raw = if ($val) { "$val" } else { $default }
return (Normalize-EnumValue $propName $raw)
}
if (-not $def.type) {
Write-Error "JSON must have 'type' field"
exit 1
@@ -411,6 +501,7 @@ function Parse-AttributeShorthand {
flags = @(if ($val.flags) { $val.flags } else { @() })
fillChecking = if ($val.fillChecking) { "$($val.fillChecking)" } else { "" }
indexing = if ($val.indexing) { "$($val.indexing)" } else { "" }
multiLine = if ($val.multiLine -eq $true) { $true } else { $false }
}
}
@@ -673,7 +764,8 @@ function Emit-Attribute {
param([string]$indent, $parsed, [string]$context)
# $context: "catalog", "document", "object", "processor", "tabular", "processor-tabular", "register"
$attrName = $parsed.name
if ($script:reservedAttrNames.ContainsKey($attrName) -or $script:reservedAttrNames.ContainsValue($attrName)) {
if ($context -notin @("tabular", "processor-tabular") -and
($script:reservedAttrNames.ContainsKey($attrName) -or $script:reservedAttrNames.ContainsValue($attrName))) {
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C."
}
$uuid = New-Guid-String
@@ -700,18 +792,20 @@ function Emit-Attribute {
X "$indent`t`t<ToolTip/>"
X "$indent`t`t<MarkNegatives>false</MarkNegatives>"
X "$indent`t`t<Mask/>"
X "$indent`t`t<MultiLine>false</MultiLine>"
$multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" }
X "$indent`t`t<MultiLine>$multiLine</MultiLine>"
X "$indent`t`t<ExtendedEdit>false</ExtendedEdit>"
X "$indent`t`t<MinValue xsi:nil=`"true`"/>"
X "$indent`t`t<MaxValue xsi:nil=`"true`"/>"
# FillFromFillingValue — not for tabular/processor (non-stored objects don't have these)
if ($context -notin @("tabular", "processor")) {
# FillFromFillingValue — not for tabular/processor/chart/register-other
# (Chart*, AccumulationRegister/AccountingRegister/CalculationRegister don't support these)
if ($context -notin @("tabular", "processor", "chart", "register-other")) {
X "$indent`t`t<FillFromFillingValue>false</FillFromFillingValue>"
}
# FillValue — not for tabular/processor
if ($context -notin @("tabular", "processor")) {
# FillValue — same restriction
if ($context -notin @("tabular", "processor", "chart", "register-other")) {
Emit-FillValue "$indent`t`t" $typeStr
}
@@ -744,7 +838,10 @@ function Emit-Attribute {
X "$indent`t`t<Indexing>$indexing</Indexing>"
X "$indent`t`t<FullTextSearch>Use</FullTextSearch>"
X "$indent`t`t<DataHistory>Use</DataHistory>"
# DataHistory — not for Chart* types and non-InformationRegister register family
if ($context -notin @("chart", "register-other")) {
X "$indent`t`t<DataHistory>Use</DataHistory>"
}
}
X "$indent`t</Properties>"
@@ -840,7 +937,8 @@ function Emit-Dimension {
X "$indent`t`t<ToolTip/>"
X "$indent`t`t<MarkNegatives>false</MarkNegatives>"
X "$indent`t`t<Mask/>"
X "$indent`t`t<MultiLine>false</MultiLine>"
$multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" }
X "$indent`t`t<MultiLine>$multiLine</MultiLine>"
X "$indent`t`t<ExtendedEdit>false</ExtendedEdit>"
X "$indent`t`t<MinValue xsi:nil=`"true`"/>"
X "$indent`t`t<MaxValue xsi:nil=`"true`"/>"
@@ -933,7 +1031,8 @@ function Emit-Resource {
X "$indent`t`t<ToolTip/>"
X "$indent`t`t<MarkNegatives>false</MarkNegatives>"
X "$indent`t`t<Mask/>"
X "$indent`t`t<MultiLine>false</MultiLine>"
$multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" }
X "$indent`t`t<MultiLine>$multiLine</MultiLine>"
X "$indent`t`t<ExtendedEdit>false</ExtendedEdit>"
X "$indent`t`t<MinValue xsi:nil=`"true`"/>"
X "$indent`t`t<MaxValue xsi:nil=`"true`"/>"
@@ -984,20 +1083,33 @@ function Emit-CatalogProperties {
X "$i<Comment/>"
$hierarchical = if ($def.hierarchical -eq $true) { "true" } else { "false" }
$hierarchyType = if ($def.hierarchyType) { "$($def.hierarchyType)" } else { "HierarchyFoldersAndItems" }
$hierarchyType = Get-EnumProp "HierarchyType" "hierarchyType" "HierarchyFoldersAndItems"
X "$i<Hierarchical>$hierarchical</Hierarchical>"
X "$i<HierarchyType>$hierarchyType</HierarchyType>"
X "$i<LimitLevelCount>false</LimitLevelCount>"
X "$i<LevelCount>2</LevelCount>"
X "$i<FoldersOnTop>true</FoldersOnTop>"
$limitLevelCount = if ($def.limitLevelCount -eq $true) { "true" } else { "false" }
$levelCount = if ($null -ne $def.levelCount) { "$($def.levelCount)" } else { "2" }
$foldersOnTop = if ($def.foldersOnTop -eq $false) { "false" } else { "true" }
X "$i<LimitLevelCount>$limitLevelCount</LimitLevelCount>"
X "$i<LevelCount>$levelCount</LevelCount>"
X "$i<FoldersOnTop>$foldersOnTop</FoldersOnTop>"
X "$i<UseStandardCommands>true</UseStandardCommands>"
X "$i<Owners/>"
X "$i<SubordinationUse>ToItems</SubordinationUse>"
if ($def.owners -and $def.owners.Count -gt 0) {
X "$i<Owners>"
foreach ($ownerRef in $def.owners) {
$fullRef = if ("$ownerRef" -match '\.') { "$ownerRef" } else { "Catalog.$ownerRef" }
X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$fullRef</xr:Item>"
}
X "$i</Owners>"
} else {
X "$i<Owners/>"
}
$subordinationUse = Get-EnumProp "SubordinationUse" "subordinationUse" "ToItems"
X "$i<SubordinationUse>$subordinationUse</SubordinationUse>"
$codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" }
$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" }
$codeType = if ($def.codeType) { "$($def.codeType)" } else { "String" }
$codeAllowedLength = if ($def.codeAllowedLength) { "$($def.codeAllowedLength)" } else { "Variable" }
$codeType = Get-EnumProp "CodeType" "codeType" "String"
$codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable"
$autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" }
$checkUnique = if ($def.checkUnique -eq $true) { "true" } else { "false" }
@@ -1005,19 +1117,22 @@ function Emit-CatalogProperties {
X "$i<DescriptionLength>$descriptionLength</DescriptionLength>"
X "$i<CodeType>$codeType</CodeType>"
X "$i<CodeAllowedLength>$codeAllowedLength</CodeAllowedLength>"
X "$i<CodeSeries>WholeCatalog</CodeSeries>"
$codeSeries = Get-EnumProp "CodeSeries" "codeSeries" "WholeCatalog"
X "$i<CodeSeries>$codeSeries</CodeSeries>"
X "$i<CheckUnique>$checkUnique</CheckUnique>"
X "$i<Autonumbering>$autonumbering</Autonumbering>"
$defaultPresentation = if ($def.defaultPresentation) { "$($def.defaultPresentation)" } else { "AsDescription" }
$defaultPresentation = Get-EnumProp "DefaultPresentation" "defaultPresentation" "AsDescription"
X "$i<DefaultPresentation>$defaultPresentation</DefaultPresentation>"
Emit-StandardAttributes $i "Catalog"
X "$i<Characteristics/>"
X "$i<PredefinedDataUpdate>Auto</PredefinedDataUpdate>"
X "$i<EditType>InDialog</EditType>"
X "$i<QuickChoice>true</QuickChoice>"
X "$i<ChoiceMode>BothWays</ChoiceMode>"
$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>"
X "$i<InputByString>"
X "$i`t<xr:Field>Catalog.$objName.StandardAttribute.Description</xr:Field>"
X "$i`t<xr:Field>Catalog.$objName.StandardAttribute.Code</xr:Field>"
@@ -1039,10 +1154,10 @@ function Emit-CatalogProperties {
X "$i<BasedOn/>"
X "$i<DataLockFields/>"
$dataLockControlMode = if ($def.dataLockControlMode) { "$($def.dataLockControlMode)" } else { "Automatic" }
$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"
$fullTextSearch = if ($def.fullTextSearch) { "$($def.fullTextSearch)" } else { "Use" }
$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"
X "$i<ObjectPresentation/>"
@@ -1067,9 +1182,9 @@ function Emit-DocumentProperties {
X "$i<UseStandardCommands>true</UseStandardCommands>"
X "$i<Numerator/>"
$numberType = if ($def.numberType) { "$($def.numberType)" } else { "String" }
$numberType = Get-EnumProp "NumberType" "numberType" "String"
$numberLength = if ($null -ne $def.numberLength) { "$($def.numberLength)" } else { "11" }
$numberAllowedLength = if ($def.numberAllowedLength) { "$($def.numberAllowedLength)" } else { "Variable" }
$numberAllowedLength = Get-EnumProp "NumberAllowedLength" "numberAllowedLength" "Variable"
$numberPeriodicity = if ($def.numberPeriodicity) { "$($def.numberPeriodicity)" } else { "Year" }
$checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" }
$autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" }
@@ -1099,10 +1214,10 @@ function Emit-DocumentProperties {
X "$i<AuxiliaryListForm/>"
X "$i<AuxiliaryChoiceForm/>"
$posting = if ($def.posting) { "$($def.posting)" } else { "Allow" }
$realTimePosting = if ($def.realTimePosting) { "$($def.realTimePosting)" } else { "Deny" }
$registerRecordsDeletion = if ($def.registerRecordsDeletion) { "$($def.registerRecordsDeletion)" } else { "AutoDelete" }
$registerRecordsWritingOnPost = if ($def.registerRecordsWritingOnPost) { "$($def.registerRecordsWritingOnPost)" } else { "WriteModified" }
$posting = Get-EnumProp "Posting" "posting" "Allow"
$realTimePosting = Get-EnumProp "RealTimePosting" "realTimePosting" "Deny"
$registerRecordsDeletion = Get-EnumProp "RegisterRecordsDeletion" "registerRecordsDeletion" "AutoDelete"
$registerRecordsWritingOnPost = Get-EnumProp "RegisterRecordsWritingOnPost" "registerRecordsWritingOnPost" "WriteModified"
$sequenceFilling = if ($def.sequenceFilling) { "$($def.sequenceFilling)" } else { "AutoFill" }
$postInPrivilegedMode = if ($def.postInPrivilegedMode -eq $false) { "false" } else { "true" }
$unpostInPrivilegedMode = if ($def.unpostInPrivilegedMode -eq $false) { "false" } else { "true" }
@@ -1148,10 +1263,10 @@ function Emit-DocumentProperties {
X "$i<IncludeHelpInContents>false</IncludeHelpInContents>"
X "$i<DataLockFields/>"
$dataLockControlMode = if ($def.dataLockControlMode) { "$($def.dataLockControlMode)" } else { "Automatic" }
$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"
$fullTextSearch = if ($def.fullTextSearch) { "$($def.fullTextSearch)" } else { "Use" }
$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"
X "$i<ObjectPresentation/>"
@@ -1177,7 +1292,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/>"
@@ -1225,7 +1341,7 @@ function Emit-ConstantProperties {
X "$i<LinkByType/>"
X "$i<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>"
$dataLockControlMode = if ($def.dataLockControlMode) { "$($def.dataLockControlMode)" } else { "Automatic" }
$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"
X "$i<DataHistory>DontUse</DataHistory>"
X "$i<UpdateDataHistoryImmediatelyAfterWrite>false</UpdateDataHistoryImmediatelyAfterWrite>"
@@ -1248,8 +1364,8 @@ function Emit-InformationRegisterProperties {
Emit-StandardAttributes $i "InformationRegister"
$periodicity = if ($def.periodicity) { "$($def.periodicity)" } else { "Nonperiodical" }
$writeMode = if ($def.writeMode) { "$($def.writeMode)" } else { "Independent" }
$periodicity = Get-EnumProp "InformationRegisterPeriodicity" "periodicity" "Nonperiodical"
$writeMode = Get-EnumProp "WriteMode" "writeMode" "Independent"
# MainFilterOnPeriod: auto based on periodicity unless explicitly set
$mainFilterOnPeriod = "false"
@@ -1264,10 +1380,10 @@ function Emit-InformationRegisterProperties {
X "$i<MainFilterOnPeriod>$mainFilterOnPeriod</MainFilterOnPeriod>"
X "$i<IncludeHelpInContents>false</IncludeHelpInContents>"
$dataLockControlMode = if ($def.dataLockControlMode) { "$($def.dataLockControlMode)" } else { "Automatic" }
$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"
$fullTextSearch = if ($def.fullTextSearch) { "$($def.fullTextSearch)" } else { "Use" }
$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"
X "$i<EnableTotalsSliceFirst>false</EnableTotalsSliceFirst>"
@@ -1293,17 +1409,17 @@ function Emit-AccumulationRegisterProperties {
X "$i<DefaultListForm/>"
X "$i<AuxiliaryListForm/>"
$registerType = if ($def.registerType) { "$($def.registerType)" } else { "Balance" }
$registerType = Get-EnumProp "RegisterType" "registerType" "Balance"
X "$i<RegisterType>$registerType</RegisterType>"
X "$i<IncludeHelpInContents>false</IncludeHelpInContents>"
Emit-StandardAttributes $i "AccumulationRegister"
$dataLockControlMode = if ($def.dataLockControlMode) { "$($def.dataLockControlMode)" } else { "Automatic" }
$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"
$fullTextSearch = if ($def.fullTextSearch) { "$($def.fullTextSearch)" } else { "Use" }
$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"
$enableTotalsSplitting = if ($def.enableTotalsSplitting -eq $false) { "false" } else { "true" }
@@ -1393,7 +1509,7 @@ function Emit-CommonModuleProperties {
X "$i<ServerCall>$serverCall</ServerCall>"
X "$i<Privileged>$privileged</Privileged>"
$returnValuesReuse = if ($def.returnValuesReuse) { "$($def.returnValuesReuse)" } else { "DontUse" }
$returnValuesReuse = Get-EnumProp "ReturnValuesReuse" "returnValuesReuse" "DontUse"
X "$i<ReturnValuesReuse>$returnValuesReuse</ReturnValuesReuse>"
}
@@ -1532,7 +1648,7 @@ function Emit-ExchangePlanProperties {
$codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" }
$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "100" }
$codeAllowedLength = if ($def.codeAllowedLength) { "$($def.codeAllowedLength)" } else { "Variable" }
$codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable"
X "$i<CodeLength>$codeLength</CodeLength>"
X "$i<CodeAllowedLength>$codeAllowedLength</CodeAllowedLength>"
@@ -1548,7 +1664,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>"
@@ -1566,10 +1683,10 @@ function Emit-ExchangePlanProperties {
X "$i<IncludeHelpInContents>false</IncludeHelpInContents>"
X "$i<DataLockFields/>"
$dataLockControlMode = if ($def.dataLockControlMode) { "$($def.dataLockControlMode)" } else { "Automatic" }
$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"
$fullTextSearch = if ($def.fullTextSearch) { "$($def.fullTextSearch)" } else { "Use" }
$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"
X "$i<ObjectPresentation/>"
@@ -1595,7 +1712,7 @@ function Emit-ChartOfCharacteristicTypesProperties {
$codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" }
$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" }
$codeAllowedLength = if ($def.codeAllowedLength) { "$($def.codeAllowedLength)" } else { "Variable" }
$codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable"
$autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" }
$checkUnique = if ($def.checkUnique -eq $true) { "true" } else { "false" }
@@ -1649,7 +1766,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>"
@@ -1672,10 +1790,10 @@ function Emit-ChartOfCharacteristicTypesProperties {
X "$i<BasedOn/>"
X "$i<DataLockFields/>"
$dataLockControlMode = if ($def.dataLockControlMode) { "$($def.dataLockControlMode)" } else { "Automatic" }
$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"
$fullTextSearch = if ($def.fullTextSearch) { "$($def.fullTextSearch)" } else { "Use" }
$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"
X "$i<ObjectPresentation/>"
@@ -1790,7 +1908,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>"
@@ -1809,10 +1928,10 @@ function Emit-ChartOfAccountsProperties {
X "$i<BasedOn/>"
X "$i<DataLockFields/>"
$dataLockControlMode = if ($def.dataLockControlMode) { "$($def.dataLockControlMode)" } else { "Automatic" }
$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"
$fullTextSearch = if ($def.fullTextSearch) { "$($def.fullTextSearch)" } else { "Use" }
$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"
X "$i<ObjectPresentation/>"
@@ -1852,10 +1971,10 @@ function Emit-AccountingRegisterProperties {
Emit-StandardAttributes $i "AccountingRegister"
$dataLockControlMode = if ($def.dataLockControlMode) { "$($def.dataLockControlMode)" } else { "Automatic" }
$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"
$fullTextSearch = if ($def.fullTextSearch) { "$($def.fullTextSearch)" } else { "Use" }
$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"
X "$i<ListPresentation/>"
@@ -1874,8 +1993,8 @@ function Emit-ChartOfCalculationTypesProperties {
$codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" }
$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" }
$codeType = if ($def.codeType) { "$($def.codeType)" } else { "String" }
$codeAllowedLength = if ($def.codeAllowedLength) { "$($def.codeAllowedLength)" } else { "Variable" }
$codeType = Get-EnumProp "CodeType" "codeType" "String"
$codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable"
X "$i<CodeLength>$codeLength</CodeLength>"
X "$i<CodeType>$codeType</CodeType>"
@@ -1883,7 +2002,7 @@ function Emit-ChartOfCalculationTypesProperties {
X "$i<DescriptionLength>$descriptionLength</DescriptionLength>"
X "$i<DefaultPresentation>AsDescription</DefaultPresentation>"
$dependence = if ($def.dependenceOnCalculationTypes) { "$($def.dependenceOnCalculationTypes)" } else { "DontUse" }
$dependence = Get-EnumProp "DependenceOnCalculationTypes" "dependenceOnCalculationTypes" "DontUse"
X "$i<DependenceOnCalculationTypes>$dependence</DependenceOnCalculationTypes>"
# BaseCalculationTypes
@@ -1906,7 +2025,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>"
@@ -1925,10 +2045,10 @@ function Emit-ChartOfCalculationTypesProperties {
X "$i<BasedOn/>"
X "$i<DataLockFields/>"
$dataLockControlMode = if ($def.dataLockControlMode) { "$($def.dataLockControlMode)" } else { "Automatic" }
$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"
$fullTextSearch = if ($def.fullTextSearch) { "$($def.fullTextSearch)" } else { "Use" }
$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"
X "$i<ObjectPresentation/>"
@@ -1955,7 +2075,7 @@ function Emit-CalculationRegisterProperties {
if ($chartOfCalcTypes) { X "$i<ChartOfCalculationTypes>$chartOfCalcTypes</ChartOfCalculationTypes>" }
else { X "$i<ChartOfCalculationTypes/>" }
$periodicity = if ($def.periodicity) { "$($def.periodicity)" } else { "Month" }
$periodicity = Get-EnumProp "InformationRegisterPeriodicity" "periodicity" "Month"
X "$i<Periodicity>$periodicity</Periodicity>"
$actionPeriod = if ($def.actionPeriod -eq $true) { "true" } else { "false" }
@@ -1977,10 +2097,10 @@ function Emit-CalculationRegisterProperties {
Emit-StandardAttributes $i "CalculationRegister"
$dataLockControlMode = if ($def.dataLockControlMode) { "$($def.dataLockControlMode)" } else { "Automatic" }
$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"
$fullTextSearch = if ($def.fullTextSearch) { "$($def.fullTextSearch)" } else { "Use" }
$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"
X "$i<ListPresentation/>"
@@ -1999,12 +2119,12 @@ function Emit-BusinessProcessProperties {
X "$i<Comment/>"
X "$i<UseStandardCommands>true</UseStandardCommands>"
$editType = if ($def.editType) { "$($def.editType)" } else { "InDialog" }
$editType = Get-EnumProp "EditType" "editType" "InDialog"
X "$i<EditType>$editType</EditType>"
$numberType = if ($def.numberType) { "$($def.numberType)" } else { "String" }
$numberType = Get-EnumProp "NumberType" "numberType" "String"
$numberLength = if ($null -ne $def.numberLength) { "$($def.numberLength)" } else { "11" }
$numberAllowedLength = if ($def.numberAllowedLength) { "$($def.numberAllowedLength)" } else { "Variable" }
$numberAllowedLength = Get-EnumProp "NumberAllowedLength" "numberAllowedLength" "Variable"
$checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" }
$autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" }
@@ -2041,10 +2161,10 @@ function Emit-BusinessProcessProperties {
X "$i<IncludeHelpInContents>false</IncludeHelpInContents>"
X "$i<DataLockFields/>"
$dataLockControlMode = if ($def.dataLockControlMode) { "$($def.dataLockControlMode)" } else { "Automatic" }
$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"
$fullTextSearch = if ($def.fullTextSearch) { "$($def.fullTextSearch)" } else { "Use" }
$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"
X "$i<ObjectPresentation/>"
@@ -2067,9 +2187,9 @@ function Emit-TaskProperties {
X "$i<Comment/>"
X "$i<UseStandardCommands>true</UseStandardCommands>"
$numberType = if ($def.numberType) { "$($def.numberType)" } else { "String" }
$numberType = Get-EnumProp "NumberType" "numberType" "String"
$numberLength = if ($null -ne $def.numberLength) { "$($def.numberLength)" } else { "14" }
$numberAllowedLength = if ($def.numberAllowedLength) { "$($def.numberAllowedLength)" } else { "Variable" }
$numberAllowedLength = Get-EnumProp "NumberAllowedLength" "numberAllowedLength" "Variable"
$checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" }
$autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" }
@@ -2114,10 +2234,10 @@ function Emit-TaskProperties {
X "$i<IncludeHelpInContents>false</IncludeHelpInContents>"
X "$i<DataLockFields/>"
$dataLockControlMode = if ($def.dataLockControlMode) { "$($def.dataLockControlMode)" } else { "Automatic" }
$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"
$fullTextSearch = if ($def.fullTextSearch) { "$($def.fullTextSearch)" } else { "Use" }
$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"
X "$i<ObjectPresentation/>"
@@ -2144,7 +2264,7 @@ function Emit-HTTPServiceProperties {
$rootURL = if ($def.rootURL) { "$($def.rootURL)" } else { $objName.ToLower() }
X "$i<RootURL>$(Esc-Xml $rootURL)</RootURL>"
$reuseSessions = if ($def.reuseSessions) { "$($def.reuseSessions)" } else { "DontUse" }
$reuseSessions = Get-EnumProp "ReuseSessions" "reuseSessions" "DontUse"
X "$i<ReuseSessions>$reuseSessions</ReuseSessions>"
$sessionMaxAge = if ($null -ne $def.sessionMaxAge) { "$($def.sessionMaxAge)" } else { "20" }
@@ -2165,7 +2285,7 @@ function Emit-WebServiceProperties {
$xdtoPackages = if ($def.xdtoPackages) { "$($def.xdtoPackages)" } else { "" }
if ($xdtoPackages) { X "$i<XDTOPackages>$xdtoPackages</XDTOPackages>" } else { X "$i<XDTOPackages/>" }
$reuseSessions = if ($def.reuseSessions) { "$($def.reuseSessions)" } else { "DontUse" }
$reuseSessions = Get-EnumProp "ReuseSessions" "reuseSessions" "DontUse"
X "$i<ReuseSessions>$reuseSessions</ReuseSessions>"
$sessionMaxAge = if ($null -ne $def.sessionMaxAge) { "$($def.sessionMaxAge)" } else { "20" }
@@ -2453,13 +2573,32 @@ function Emit-AddressingAttribute {
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# --- 14a. Detect format version from existing Configuration.xml ---
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"
}
$script:formatVersion = Detect-FormatVersion $OutputDir
# --- 15. Main assembler ---
$uuid = New-Guid-String
# XML declaration
X '<?xml version="1.0" encoding="UTF-8"?>'
X "<MetaDataObject $($script:xmlnsDecl) version=`"2.17`">"
X "<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">"
X "`t<$objType uuid=`"$uuid`">"
# InternalInfo
@@ -2549,6 +2688,7 @@ if ($objType -in $typesWithAttrTS) {
"Catalog" { "catalog" }
"Document" { "document" }
{ $_ -in @("DataProcessor","Report") } { "processor" }
{ $_ -in @("ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes") } { "chart" }
default { "object" }
}
foreach ($a in $attrs) {
@@ -2559,10 +2699,12 @@ if ($objType -in $typesWithAttrTS) {
Emit-TabularSection "`t`t`t" $tsName $columns $objType $objName
}
foreach ($af in $acctFlags) {
Emit-AccountingFlag "`t`t`t" "$af"
$afName = if ($af.name) { $af.name } else { "$af" }
Emit-AccountingFlag "`t`t`t" $afName
}
foreach ($edf in $extDimFlags) {
Emit-ExtDimensionAccountingFlag "`t`t`t" "$edf"
$edfName = if ($edf.name) { $edf.name } else { "$edf" }
Emit-ExtDimensionAccountingFlag "`t`t`t" $edfName
}
foreach ($aa in $addrAttrs) {
Emit-AddressingAttribute "`t`t`t" $aa
@@ -2625,8 +2767,11 @@ if ($objType -in @("InformationRegister","AccumulationRegister","AccountingRegis
foreach ($d in $dims) {
Emit-Dimension "`t`t`t" $d $objType
}
# InformationRegister.Attribute supports FillFromFillingValue/FillValue/DataHistory;
# AccumulationRegister/AccountingRegister/CalculationRegister.Attribute do NOT.
$regCtx = if ($objType -eq "InformationRegister") { "register-info" } else { "register-other" }
foreach ($a in $regAttrs) {
Emit-Attribute "`t`t`t" $a "register"
Emit-Attribute "`t`t`t" $a $regCtx
}
X "`t`t</ChildObjects>"
} else {
@@ -2743,8 +2888,8 @@ if (-not (Test-Path $typeDir)) {
New-Item -ItemType Directory -Path $typeDir -Force | Out-Null
}
if ($objType -notin $typesNoSubDir) {
if (-not (Test-Path $extDir)) {
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
if (-not (Test-Path $objSubDir)) {
New-Item -ItemType Directory -Path $objSubDir -Force | Out-Null
}
}
@@ -2754,6 +2899,13 @@ $enc = New-Object System.Text.UTF8Encoding($true)
# Module files
$modulesCreated = @()
# Helper: create Ext/ only when needed (avoids empty Ext/ for Constant, Enum, etc.)
function Ensure-ExtDir {
if (-not (Test-Path $extDir)) {
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
}
}
# Types with ObjectModule.bsl
$typesWithObjectModule = @("Catalog","Document","Report","DataProcessor","ExchangePlan",
"ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes",
@@ -2761,13 +2913,16 @@ $typesWithObjectModule = @("Catalog","Document","Report","DataProcessor","Exchan
# Types with RecordSetModule.bsl
$typesWithRecordSetModule = @("InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister")
# Types with ManagerModule.bsl
$typesWithManagerModule = @("Report","DataProcessor")
$typesWithManagerModule = @("Report","DataProcessor","Constant","Enum")
# Types with ValueManagerModule.bsl
$typesWithValueManagerModule = @("Constant")
# Types with Module.bsl (general)
$typesWithModule = @("CommonModule","HTTPService","WebService")
if ($objType -in $typesWithObjectModule) {
$modulePath = Join-Path $extDir "ObjectModule.bsl"
if (-not (Test-Path $modulePath)) {
Ensure-ExtDir
[System.IO.File]::WriteAllText($modulePath, "", $enc)
$modulesCreated += $modulePath
}
@@ -2775,6 +2930,15 @@ if ($objType -in $typesWithObjectModule) {
if ($objType -in $typesWithManagerModule) {
$modulePath = Join-Path $extDir "ManagerModule.bsl"
if (-not (Test-Path $modulePath)) {
Ensure-ExtDir
[System.IO.File]::WriteAllText($modulePath, "", $enc)
$modulesCreated += $modulePath
}
}
if ($objType -in $typesWithValueManagerModule) {
$modulePath = Join-Path $extDir "ValueManagerModule.bsl"
if (-not (Test-Path $modulePath)) {
Ensure-ExtDir
[System.IO.File]::WriteAllText($modulePath, "", $enc)
$modulesCreated += $modulePath
}
@@ -2782,6 +2946,7 @@ if ($objType -in $typesWithManagerModule) {
if ($objType -in $typesWithRecordSetModule) {
$modulePath = Join-Path $extDir "RecordSetModule.bsl"
if (-not (Test-Path $modulePath)) {
Ensure-ExtDir
[System.IO.File]::WriteAllText($modulePath, "", $enc)
$modulesCreated += $modulePath
}
@@ -2789,6 +2954,7 @@ if ($objType -in $typesWithRecordSetModule) {
if ($objType -in $typesWithModule) {
$modulePath = Join-Path $extDir "Module.bsl"
if (-not (Test-Path $modulePath)) {
Ensure-ExtDir
[System.IO.File]::WriteAllText($modulePath, "", $enc)
$modulesCreated += $modulePath
}
@@ -2798,7 +2964,8 @@ if ($objType -in $typesWithModule) {
if ($objType -eq "ExchangePlan") {
$contentPath = Join-Path $extDir "Content.xml"
if (-not (Test-Path $contentPath)) {
$contentXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<ExchangePlanContent xmlns=`"http://v8.1c.ru/8.3/xcf/extrnprops`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" version=`"2.17`"/>`r`n"
Ensure-ExtDir
$contentXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<ExchangePlanContent xmlns=`"http://v8.1c.ru/8.3/xcf/extrnprops`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" version=`"$($script:formatVersion)`"/>`r`n"
[System.IO.File]::WriteAllText($contentPath, $contentXml, $enc)
$modulesCreated += $contentPath
}
@@ -2806,7 +2973,8 @@ if ($objType -eq "ExchangePlan") {
if ($objType -eq "BusinessProcess") {
$flowchartPath = Join-Path $extDir "Flowchart.xml"
if (-not (Test-Path $flowchartPath)) {
$flowchartXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Flowchart xmlns=`"http://v8.1c.ru/8.3/MDClasses`" version=`"2.17`"/>`r`n"
Ensure-ExtDir
$flowchartXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Flowchart xmlns=`"http://v8.1c.ru/8.3/MDClasses`" version=`"$($script:formatVersion)`"/>`r`n"
[System.IO.File]::WriteAllText($flowchartPath, $flowchartXml, $enc)
$modulesCreated += $flowchartPath
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-compile v1.3 — Compile 1C metadata object from JSON
# meta-compile v1.11 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -139,6 +139,89 @@ object_type_synonyms = {
'ОпределяемыйТип': 'DefinedType',
}
# Enum property value synonyms — model often gets these slightly wrong
enum_value_aliases = {
# RegisterType (AccumulationRegister)
'Balances': 'Balance', 'Остатки': 'Balance', 'Обороты': 'Turnovers',
# WriteMode (InformationRegister)
'RecordSubordinate': 'RecorderSubordinate', 'Subordinate': 'RecorderSubordinate',
'ПодчинениеРегистратору': 'RecorderSubordinate', 'Независимый': 'Independent',
# DependenceOnCalculationTypes (ChartOfCalculationTypes)
'NotDependOnCalculationTypes': 'DontUse', 'NoDependence': 'DontUse', 'NotUsed': 'DontUse',
'Depend': 'OnActionPeriod', 'ПоПериодуДействия': 'OnActionPeriod',
# InformationRegisterPeriodicity
'None': 'Nonperiodical', 'Daily': 'Day', 'Monthly': 'Month',
'Quarterly': 'Quarter', 'Yearly': 'Year',
'Непериодический': 'Nonperiodical', 'Секунда': 'Second', 'День': 'Day',
'Месяц': 'Month', 'Квартал': 'Quarter', 'Год': 'Year',
'ПозицияРегистратора': 'RecorderPosition',
# DataLockControlMode
'Автоматический': 'Automatic', 'Управляемый': 'Managed',
# FullTextSearch
'Использовать': 'Use', 'НеИспользовать': 'DontUse',
# Posting
'Разрешить': 'Allow', 'Запретить': 'Deny',
# EditType
'ВДиалоге': 'InDialog', 'ВСписке': 'InList', 'ОбаСпособа': 'BothWays',
# DefaultPresentation
'ВВидеНаименования': 'AsDescription', 'ВВидеКода': 'AsCode',
# FillChecking
'НеПроверять': 'DontCheck', 'Ошибка': 'ShowError', 'Предупреждение': 'ShowWarning',
# Indexing
'НеИндексировать': 'DontIndex', 'Индексировать': 'Index',
'ИндексироватьСДопУпорядочиванием': 'IndexWithAdditionalOrder',
}
# Valid enum values per property (from meta-validate)
valid_enum_values = {
'RegisterType': ['Balance', 'Turnovers'],
'WriteMode': ['Independent', 'RecorderSubordinate'],
'InformationRegisterPeriodicity': ['Nonperiodical', 'Second', 'Day', 'Month', 'Quarter', 'Year', 'RecorderPosition'],
'DependenceOnCalculationTypes': ['DontUse', 'OnActionPeriod'],
'DataLockControlMode': ['Automatic', 'Managed'],
'FullTextSearch': ['Use', 'DontUse'],
'DataHistory': ['Use', 'DontUse'],
'DefaultPresentation': ['AsDescription', 'AsCode'],
'Posting': ['Allow', 'Deny'],
'RealTimePosting': ['Allow', 'Deny'],
'EditType': ['InDialog', 'InList', 'BothWays'],
'HierarchyType': ['HierarchyFoldersAndItems', 'HierarchyItemsOnly'],
'CodeType': ['String', 'Number'],
'CodeAllowedLength': ['Variable', 'Fixed'],
'NumberType': ['String', 'Number'],
'NumberAllowedLength': ['Variable', 'Fixed'],
'RegisterRecordsDeletion': ['AutoDelete', 'AutoDeleteOnUnpost', 'AutoDeleteOff'],
'RegisterRecordsWritingOnPost': ['WriteModified', 'WriteSelected', 'WriteAll'],
'ReturnValuesReuse': ['DontUse', 'DuringRequest', 'DuringSession'],
'ReuseSessions': ['DontUse', 'AutoUse'],
'FillChecking': ['DontCheck', 'ShowError', 'ShowWarning'],
'Indexing': ['DontIndex', 'Index', 'IndexWithAdditionalOrder'],
'SubordinationUse': ['ToItems', 'ToFolders', 'ToFoldersAndItems'],
'CodeSeries': ['WholeCatalog', 'WithinSubordination'],
'ChoiceMode': ['BothWays', 'QuickChoice', 'FromForm'],
}
def normalize_enum_value(prop_name, value):
# 1. Check alias dictionary — silent auto-correct
if value in enum_value_aliases:
return enum_value_aliases[value]
# 2. Case-insensitive match against valid values — silent
valid = valid_enum_values.get(prop_name)
if valid:
for v in valid:
if v.lower() == value.lower():
return v
# 3. Known property, unknown value — error with hint
print(f"Invalid value '{value}' for property '{prop_name}'. Valid values: {', '.join(valid)}", file=sys.stderr)
sys.exit(1)
# 4. Unknown property — pass-through (no validation data)
return value
def get_enum_prop(prop_name, field_name, default):
val = defn.get(field_name)
raw = str(val) if val else default
return normalize_enum_value(prop_name, raw)
if not defn.get('type'):
print("JSON must have 'type' field", file=sys.stderr)
sys.exit(1)
@@ -381,6 +464,7 @@ def parse_attribute_shorthand(val):
'flags': list(val.get('flags', [])),
'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,
}
def parse_enum_value_shorthand(val):
@@ -645,7 +729,7 @@ RESERVED_ATTR_NAMES_RU = {
def emit_attribute(indent, parsed, context):
attr_name = parsed['name']
if attr_name in RESERVED_ATTR_NAMES or attr_name in RESERVED_ATTR_NAMES_RU:
if context not in ('tabular', 'processor-tabular') and (attr_name in RESERVED_ATTR_NAMES or attr_name in RESERVED_ATTR_NAMES_RU):
print(f"WARNING: Attribute '{attr_name}' conflicts with a standard attribute name. This may cause errors when loading into 1C.", file=sys.stderr)
uid = new_uuid()
X(f'{indent}<Attribute uuid="{uid}">')
@@ -666,13 +750,16 @@ def emit_attribute(indent, parsed, context):
X(f'{indent}\t\t<ToolTip/>')
X(f'{indent}\t\t<MarkNegatives>false</MarkNegatives>')
X(f'{indent}\t\t<Mask/>')
X(f'{indent}\t\t<MultiLine>false</MultiLine>')
multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false'
X(f'{indent}\t\t<MultiLine>{multi_line}</MultiLine>')
X(f'{indent}\t\t<ExtendedEdit>false</ExtendedEdit>')
X(f'{indent}\t\t<MinValue xsi:nil="true"/>')
X(f'{indent}\t\t<MaxValue xsi:nil="true"/>')
if context not in ('tabular', 'processor'):
# FillFromFillingValue / FillValue — not for tabular/processor/chart/register-other
# (Chart*, AccumulationRegister/AccountingRegister/CalculationRegister don't support these)
if context not in ('tabular', 'processor', 'chart', 'register-other'):
X(f'{indent}\t\t<FillFromFillingValue>false</FillFromFillingValue>')
if context not in ('tabular', 'processor'):
if context not in ('tabular', 'processor', 'chart', 'register-other'):
emit_fill_value(f'{indent}\t\t', type_str)
fill_checking = 'DontCheck'
if 'req' in parsed.get('flags', []):
@@ -700,7 +787,9 @@ def emit_attribute(indent, parsed, context):
indexing = parsed['indexing']
X(f'{indent}\t\t<Indexing>{indexing}</Indexing>')
X(f'{indent}\t\t<FullTextSearch>Use</FullTextSearch>')
X(f'{indent}\t\t<DataHistory>Use</DataHistory>')
# DataHistory — not for Chart* types and non-InformationRegister register family
if context not in ('chart', 'register-other'):
X(f'{indent}\t\t<DataHistory>Use</DataHistory>')
X(f'{indent}\t</Properties>')
X(f'{indent}</Attribute>')
@@ -780,7 +869,8 @@ def emit_dimension(indent, parsed, register_type):
X(f'{indent}\t\t<ToolTip/>')
X(f'{indent}\t\t<MarkNegatives>false</MarkNegatives>')
X(f'{indent}\t\t<Mask/>')
X(f'{indent}\t\t<MultiLine>false</MultiLine>')
multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false'
X(f'{indent}\t\t<MultiLine>{multi_line}</MultiLine>')
X(f'{indent}\t\t<ExtendedEdit>false</ExtendedEdit>')
X(f'{indent}\t\t<MinValue xsi:nil="true"/>')
X(f'{indent}\t\t<MaxValue xsi:nil="true"/>')
@@ -853,7 +943,8 @@ def emit_resource(indent, parsed, register_type):
X(f'{indent}\t\t<ToolTip/>')
X(f'{indent}\t\t<MarkNegatives>false</MarkNegatives>')
X(f'{indent}\t\t<Mask/>')
X(f'{indent}\t\t<MultiLine>false</MultiLine>')
multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false'
X(f'{indent}\t\t<MultiLine>{multi_line}</MultiLine>')
X(f'{indent}\t\t<ExtendedEdit>false</ExtendedEdit>')
X(f'{indent}\t\t<MinValue xsi:nil="true"/>')
X(f'{indent}\t\t<MaxValue xsi:nil="true"/>')
@@ -892,36 +983,51 @@ def emit_catalog_properties(indent):
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
hierarchical = 'true' if defn.get('hierarchical') is True else 'false'
hierarchy_type = str(defn['hierarchyType']) if defn.get('hierarchyType') else 'HierarchyFoldersAndItems'
hierarchy_type = get_enum_prop('HierarchyType', 'hierarchyType', 'HierarchyFoldersAndItems')
X(f'{i}<Hierarchical>{hierarchical}</Hierarchical>')
X(f'{i}<HierarchyType>{hierarchy_type}</HierarchyType>')
X(f'{i}<LimitLevelCount>false</LimitLevelCount>')
X(f'{i}<LevelCount>2</LevelCount>')
X(f'{i}<FoldersOnTop>true</FoldersOnTop>')
limit_level_count = 'true' if defn.get('limitLevelCount') is True else 'false'
level_count = str(defn['levelCount']) if defn.get('levelCount') is not None else '2'
folders_on_top = 'false' if defn.get('foldersOnTop') is False else 'true'
X(f'{i}<LimitLevelCount>{limit_level_count}</LimitLevelCount>')
X(f'{i}<LevelCount>{level_count}</LevelCount>')
X(f'{i}<FoldersOnTop>{folders_on_top}</FoldersOnTop>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
X(f'{i}<Owners/>')
X(f'{i}<SubordinationUse>ToItems</SubordinationUse>')
owners = defn.get('owners', [])
if owners:
X(f'{i}<Owners>')
for owner_ref in owners:
full_ref = owner_ref if '.' in str(owner_ref) else f'Catalog.{owner_ref}'
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{full_ref}</xr:Item>')
X(f'{i}</Owners>')
else:
X(f'{i}<Owners/>')
subordination_use = get_enum_prop('SubordinationUse', 'subordinationUse', 'ToItems')
X(f'{i}<SubordinationUse>{subordination_use}</SubordinationUse>')
code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '9'
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '25'
code_type = str(defn['codeType']) if defn.get('codeType') else 'String'
code_allowed_length = str(defn['codeAllowedLength']) if defn.get('codeAllowedLength') else 'Variable'
code_type = get_enum_prop('CodeType', 'codeType', 'String')
code_allowed_length = get_enum_prop('CodeAllowedLength', 'codeAllowedLength', 'Variable')
autonumbering = 'false' if defn.get('autonumbering') is False else 'true'
check_unique = 'true' if defn.get('checkUnique') is True else 'false'
X(f'{i}<CodeLength>{code_length}</CodeLength>')
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
X(f'{i}<CodeType>{code_type}</CodeType>')
X(f'{i}<CodeAllowedLength>{code_allowed_length}</CodeAllowedLength>')
X(f'{i}<CodeSeries>WholeCatalog</CodeSeries>')
code_series = get_enum_prop('CodeSeries', 'codeSeries', 'WholeCatalog')
X(f'{i}<CodeSeries>{code_series}</CodeSeries>')
X(f'{i}<CheckUnique>{check_unique}</CheckUnique>')
X(f'{i}<Autonumbering>{autonumbering}</Autonumbering>')
default_presentation = str(defn['defaultPresentation']) if defn.get('defaultPresentation') else 'AsDescription'
default_presentation = get_enum_prop('DefaultPresentation', 'defaultPresentation', 'AsDescription')
X(f'{i}<DefaultPresentation>{default_presentation}</DefaultPresentation>')
emit_standard_attributes(i, 'Catalog')
X(f'{i}<Characteristics/>')
X(f'{i}<PredefinedDataUpdate>Auto</PredefinedDataUpdate>')
X(f'{i}<EditType>InDialog</EditType>')
X(f'{i}<QuickChoice>true</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
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>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>Catalog.{obj_name}.StandardAttribute.Description</xr:Field>')
X(f'{i}\t<xr:Field>Catalog.{obj_name}.StandardAttribute.Code</xr:Field>')
@@ -942,9 +1048,9 @@ def emit_catalog_properties(indent):
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<BasedOn/>')
X(f'{i}<DataLockFields/>')
data_lock_control_mode = str(defn['dataLockControlMode']) if defn.get('dataLockControlMode') else 'Automatic'
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = str(defn['fullTextSearch']) if defn.get('fullTextSearch') else 'Use'
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<ObjectPresentation/>')
X(f'{i}<ExtendedObjectPresentation/>')
@@ -964,10 +1070,10 @@ def emit_document_properties(indent):
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
X(f'{i}<Numerator/>')
number_type = str(defn['numberType']) if defn.get('numberType') else 'String'
number_type = get_enum_prop('NumberType', 'numberType', 'String')
number_length = str(defn['numberLength']) if defn.get('numberLength') is not None else '11'
number_allowed_length = str(defn['numberAllowedLength']) if defn.get('numberAllowedLength') else 'Variable'
number_periodicity = str(defn['numberPeriodicity']) if defn.get('numberPeriodicity') else 'Year'
number_allowed_length = get_enum_prop('NumberAllowedLength', 'numberAllowedLength', 'Variable')
number_periodicity = get_enum_prop('InformationRegisterPeriodicity', 'numberPeriodicity', 'Year')
check_unique = 'false' if defn.get('checkUnique') is False else 'true'
autonumbering = 'false' if defn.get('autonumbering') is False else 'true'
X(f'{i}<NumberType>{number_type}</NumberType>')
@@ -992,10 +1098,10 @@ def emit_document_properties(indent):
X(f'{i}<AuxiliaryObjectForm/>')
X(f'{i}<AuxiliaryListForm/>')
X(f'{i}<AuxiliaryChoiceForm/>')
posting = str(defn['posting']) if defn.get('posting') else 'Allow'
real_time_posting = str(defn['realTimePosting']) if defn.get('realTimePosting') else 'Deny'
reg_records_deletion = str(defn['registerRecordsDeletion']) if defn.get('registerRecordsDeletion') else 'AutoDelete'
reg_records_writing = str(defn['registerRecordsWritingOnPost']) if defn.get('registerRecordsWritingOnPost') else 'WriteModified'
posting = get_enum_prop('Posting', 'posting', 'Allow')
real_time_posting = get_enum_prop('RealTimePosting', 'realTimePosting', 'Deny')
reg_records_deletion = get_enum_prop('RegisterRecordsDeletion', 'registerRecordsDeletion', 'AutoDelete')
reg_records_writing = get_enum_prop('RegisterRecordsWritingOnPost', 'registerRecordsWritingOnPost', 'WriteModified')
sequence_filling = str(defn['sequenceFilling']) if defn.get('sequenceFilling') else 'AutoFill'
post_in_priv = 'false' if defn.get('postInPrivilegedMode') is False else 'true'
unpost_in_priv = 'false' if defn.get('unpostInPrivilegedMode') is False else 'true'
@@ -1029,9 +1135,9 @@ def emit_document_properties(indent):
X(f'{i}<UnpostInPrivilegedMode>{unpost_in_priv}</UnpostInPrivilegedMode>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<DataLockFields/>')
data_lock_control_mode = str(defn['dataLockControlMode']) if defn.get('dataLockControlMode') else 'Automatic'
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = str(defn['fullTextSearch']) if defn.get('fullTextSearch') else 'Use'
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<ObjectPresentation/>')
X(f'{i}<ExtendedObjectPresentation/>')
@@ -1051,7 +1157,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/>')
@@ -1092,7 +1199,7 @@ def emit_constant_properties(indent):
X(f'{i}<ChoiceForm/>')
X(f'{i}<LinkByType/>')
X(f'{i}<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>')
data_lock_control_mode = str(defn['dataLockControlMode']) if defn.get('dataLockControlMode') else 'Automatic'
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
X(f'{i}<DataHistory>DontUse</DataHistory>')
X(f'{i}<UpdateDataHistoryImmediatelyAfterWrite>false</UpdateDataHistoryImmediatelyAfterWrite>')
@@ -1110,8 +1217,8 @@ def emit_information_register_properties(indent):
X(f'{i}<AuxiliaryRecordForm/>')
X(f'{i}<AuxiliaryListForm/>')
emit_standard_attributes(i, 'InformationRegister')
periodicity = str(defn['periodicity']) if defn.get('periodicity') else 'Nonperiodical'
write_mode = str(defn['writeMode']) if defn.get('writeMode') else 'Independent'
periodicity = get_enum_prop('InformationRegisterPeriodicity', 'periodicity', 'Nonperiodical')
write_mode = get_enum_prop('WriteMode', 'writeMode', 'Independent')
main_filter_on_period = 'false'
if defn.get('mainFilterOnPeriod') is not None:
main_filter_on_period = 'true' if defn['mainFilterOnPeriod'] is True else 'false'
@@ -1121,9 +1228,9 @@ def emit_information_register_properties(indent):
X(f'{i}<WriteMode>{write_mode}</WriteMode>')
X(f'{i}<MainFilterOnPeriod>{main_filter_on_period}</MainFilterOnPeriod>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
data_lock_control_mode = str(defn['dataLockControlMode']) if defn.get('dataLockControlMode') else 'Automatic'
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = str(defn['fullTextSearch']) if defn.get('fullTextSearch') else 'Use'
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<EnableTotalsSliceFirst>false</EnableTotalsSliceFirst>')
X(f'{i}<EnableTotalsSliceLast>false</EnableTotalsSliceLast>')
@@ -1144,13 +1251,13 @@ def emit_accumulation_register_properties(indent):
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
X(f'{i}<DefaultListForm/>')
X(f'{i}<AuxiliaryListForm/>')
register_type = str(defn['registerType']) if defn.get('registerType') else 'Balance'
register_type = get_enum_prop('RegisterType', 'registerType', 'Balance')
X(f'{i}<RegisterType>{register_type}</RegisterType>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
emit_standard_attributes(i, 'AccumulationRegister')
data_lock_control_mode = str(defn['dataLockControlMode']) if defn.get('dataLockControlMode') else 'Automatic'
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = str(defn['fullTextSearch']) if defn.get('fullTextSearch') else 'Use'
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
enable_totals_splitting = 'false' if defn.get('enableTotalsSplitting') is False else 'true'
X(f'{i}<EnableTotalsSplitting>{enable_totals_splitting}</EnableTotalsSplitting>')
@@ -1231,7 +1338,7 @@ def emit_common_module_properties(indent):
X(f'{i}<ClientOrdinaryApplication>{client_ordinary}</ClientOrdinaryApplication>')
X(f'{i}<ServerCall>{server_call}</ServerCall>')
X(f'{i}<Privileged>{privileged}</Privileged>')
return_values_reuse = str(defn['returnValuesReuse']) if defn.get('returnValuesReuse') else 'DontUse'
return_values_reuse = get_enum_prop('ReturnValuesReuse', 'returnValuesReuse', 'DontUse')
X(f'{i}<ReturnValuesReuse>{return_values_reuse}</ReturnValuesReuse>')
def emit_scheduled_job_properties(indent):
@@ -1353,7 +1460,7 @@ def emit_exchange_plan_properties(indent):
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '9'
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '100'
code_allowed_length = str(defn['codeAllowedLength']) if defn.get('codeAllowedLength') else 'Variable'
code_allowed_length = get_enum_prop('CodeAllowedLength', 'codeAllowedLength', 'Variable')
X(f'{i}<CodeLength>{code_length}</CodeLength>')
X(f'{i}<CodeAllowedLength>{code_allowed_length}</CodeAllowedLength>')
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
@@ -1365,7 +1472,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>')
@@ -1382,9 +1490,9 @@ def emit_exchange_plan_properties(indent):
X(f'{i}<AuxiliaryChoiceForm/>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<DataLockFields/>')
data_lock_control_mode = str(defn['dataLockControlMode']) if defn.get('dataLockControlMode') else 'Automatic'
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = str(defn['fullTextSearch']) if defn.get('fullTextSearch') else 'Use'
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<ObjectPresentation/>')
X(f'{i}<ExtendedObjectPresentation/>')
@@ -1405,7 +1513,7 @@ def emit_chart_of_characteristic_types_properties(indent):
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '9'
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '25'
code_allowed_length = str(defn['codeAllowedLength']) if defn.get('codeAllowedLength') else 'Variable'
code_allowed_length = get_enum_prop('CodeAllowedLength', 'codeAllowedLength', 'Variable')
autonumbering = 'false' if defn.get('autonumbering') is False else 'true'
check_unique = 'true' if defn.get('checkUnique') is True else 'false'
X(f'{i}<CodeLength>{code_length}</CodeLength>')
@@ -1451,7 +1559,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>')
@@ -1473,9 +1582,9 @@ def emit_chart_of_characteristic_types_properties(indent):
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<BasedOn/>')
X(f'{i}<DataLockFields/>')
data_lock_control_mode = str(defn['dataLockControlMode']) if defn.get('dataLockControlMode') else 'Automatic'
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = str(defn['fullTextSearch']) if defn.get('fullTextSearch') else 'Use'
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<ObjectPresentation/>')
X(f'{i}<ExtendedObjectPresentation/>')
@@ -1567,7 +1676,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>')
@@ -1585,9 +1695,9 @@ def emit_chart_of_accounts_properties(indent):
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<BasedOn/>')
X(f'{i}<DataLockFields/>')
data_lock_control_mode = str(defn['dataLockControlMode']) if defn.get('dataLockControlMode') else 'Automatic'
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = str(defn['fullTextSearch']) if defn.get('fullTextSearch') else 'Use'
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<ObjectPresentation/>')
X(f'{i}<ExtendedObjectPresentation/>')
@@ -1619,9 +1729,9 @@ def emit_accounting_register_properties(indent):
X(f'{i}<PeriodAdjustmentLength>{period_adj_len}</PeriodAdjustmentLength>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
emit_standard_attributes(i, 'AccountingRegister')
data_lock_control_mode = str(defn['dataLockControlMode']) if defn.get('dataLockControlMode') else 'Automatic'
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = str(defn['fullTextSearch']) if defn.get('fullTextSearch') else 'Use'
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<ListPresentation/>')
X(f'{i}<ExtendedListPresentation/>')
@@ -1635,14 +1745,14 @@ def emit_chart_of_calculation_types_properties(indent):
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '9'
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '25'
code_type = str(defn['codeType']) if defn.get('codeType') else 'String'
code_allowed_length = str(defn['codeAllowedLength']) if defn.get('codeAllowedLength') else 'Variable'
code_type = get_enum_prop('CodeType', 'codeType', 'String')
code_allowed_length = get_enum_prop('CodeAllowedLength', 'codeAllowedLength', 'Variable')
X(f'{i}<CodeLength>{code_length}</CodeLength>')
X(f'{i}<CodeType>{code_type}</CodeType>')
X(f'{i}<CodeAllowedLength>{code_allowed_length}</CodeAllowedLength>')
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
X(f'{i}<DefaultPresentation>AsDescription</DefaultPresentation>')
dependence = str(defn['dependenceOnCalculationTypes']) if defn.get('dependenceOnCalculationTypes') else 'DontUse'
dependence = get_enum_prop('DependenceOnCalculationTypes', 'dependenceOnCalculationTypes', 'DontUse')
X(f'{i}<DependenceOnCalculationTypes>{dependence}</DependenceOnCalculationTypes>')
base_types = list(defn.get('baseCalculationTypes', []))
if base_types:
@@ -1658,7 +1768,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>')
@@ -1676,9 +1787,9 @@ def emit_chart_of_calculation_types_properties(indent):
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<BasedOn/>')
X(f'{i}<DataLockFields/>')
data_lock_control_mode = str(defn['dataLockControlMode']) if defn.get('dataLockControlMode') else 'Automatic'
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = str(defn['fullTextSearch']) if defn.get('fullTextSearch') else 'Use'
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<ObjectPresentation/>')
X(f'{i}<ExtendedObjectPresentation/>')
@@ -1701,7 +1812,7 @@ def emit_calculation_register_properties(indent):
X(f'{i}<ChartOfCalculationTypes>{chart_of_calc_types}</ChartOfCalculationTypes>')
else:
X(f'{i}<ChartOfCalculationTypes/>')
periodicity = str(defn['periodicity']) if defn.get('periodicity') else 'Month'
periodicity = get_enum_prop('InformationRegisterPeriodicity', 'periodicity', 'Month')
X(f'{i}<Periodicity>{periodicity}</Periodicity>')
action_period = 'true' if defn.get('actionPeriod') is True else 'false'
X(f'{i}<ActionPeriod>{action_period}</ActionPeriod>')
@@ -1724,9 +1835,9 @@ def emit_calculation_register_properties(indent):
X(f'{i}<ScheduleDate/>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
emit_standard_attributes(i, 'CalculationRegister')
data_lock_control_mode = str(defn['dataLockControlMode']) if defn.get('dataLockControlMode') else 'Automatic'
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = str(defn['fullTextSearch']) if defn.get('fullTextSearch') else 'Use'
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<ListPresentation/>')
X(f'{i}<ExtendedListPresentation/>')
@@ -1738,11 +1849,11 @@ def emit_business_process_properties(indent):
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
edit_type = str(defn['editType']) if defn.get('editType') else 'InDialog'
edit_type = get_enum_prop('EditType', 'editType', 'InDialog')
X(f'{i}<EditType>{edit_type}</EditType>')
number_type = str(defn['numberType']) if defn.get('numberType') else 'String'
number_type = get_enum_prop('NumberType', 'numberType', 'String')
number_length = str(defn['numberLength']) if defn.get('numberLength') is not None else '11'
number_allowed_length = str(defn['numberAllowedLength']) if defn.get('numberAllowedLength') else 'Variable'
number_allowed_length = get_enum_prop('NumberAllowedLength', 'numberAllowedLength', 'Variable')
check_unique = 'false' if defn.get('checkUnique') is False else 'true'
autonumbering = 'false' if defn.get('autonumbering') is False else 'true'
X(f'{i}<NumberType>{number_type}</NumberType>')
@@ -1773,9 +1884,9 @@ def emit_business_process_properties(indent):
X(f'{i}<AuxiliaryChoiceForm/>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<DataLockFields/>')
data_lock_control_mode = str(defn['dataLockControlMode']) if defn.get('dataLockControlMode') else 'Automatic'
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = str(defn['fullTextSearch']) if defn.get('fullTextSearch') else 'Use'
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<ObjectPresentation/>')
X(f'{i}<ExtendedObjectPresentation/>')
@@ -1793,9 +1904,9 @@ def emit_task_properties(indent):
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
number_type = str(defn['numberType']) if defn.get('numberType') else 'String'
number_type = get_enum_prop('NumberType', 'numberType', 'String')
number_length = str(defn['numberLength']) if defn.get('numberLength') is not None else '14'
number_allowed_length = str(defn['numberAllowedLength']) if defn.get('numberAllowedLength') else 'Variable'
number_allowed_length = get_enum_prop('NumberAllowedLength', 'numberAllowedLength', 'Variable')
check_unique = 'false' if defn.get('checkUnique') is False else 'true'
autonumbering = 'false' if defn.get('autonumbering') is False else 'true'
task_number_auto_prefix = str(defn['taskNumberAutoPrefix']) if defn.get('taskNumberAutoPrefix') else 'BusinessProcessNumber'
@@ -1840,9 +1951,9 @@ def emit_task_properties(indent):
X(f'{i}<AuxiliaryChoiceForm/>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<DataLockFields/>')
data_lock_control_mode = str(defn['dataLockControlMode']) if defn.get('dataLockControlMode') else 'Automatic'
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = str(defn['fullTextSearch']) if defn.get('fullTextSearch') else 'Use'
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<ObjectPresentation/>')
X(f'{i}<ExtendedObjectPresentation/>')
@@ -1861,7 +1972,7 @@ def emit_http_service_properties(indent):
X(f'{i}<Comment/>')
root_url = str(defn['rootURL']) if defn.get('rootURL') else obj_name.lower()
X(f'{i}<RootURL>{esc_xml(root_url)}</RootURL>')
reuse_sessions = str(defn['reuseSessions']) if defn.get('reuseSessions') else 'DontUse'
reuse_sessions = get_enum_prop('ReuseSessions', 'reuseSessions', 'DontUse')
X(f'{i}<ReuseSessions>{reuse_sessions}</ReuseSessions>')
session_max_age = str(defn['sessionMaxAge']) if defn.get('sessionMaxAge') is not None else '20'
X(f'{i}<SessionMaxAge>{session_max_age}</SessionMaxAge>')
@@ -1878,7 +1989,7 @@ def emit_web_service_properties(indent):
X(f'{i}<XDTOPackages>{xdto_packages}</XDTOPackages>')
else:
X(f'{i}<XDTOPackages/>')
reuse_sessions = str(defn['reuseSessions']) if defn.get('reuseSessions') else 'DontUse'
reuse_sessions = get_enum_prop('ReuseSessions', 'reuseSessions', 'DontUse')
X(f'{i}<ReuseSessions>{reuse_sessions}</ReuseSessions>')
session_max_age = str(defn['sessionMaxAge']) if defn.get('sessionMaxAge') is not None else '20'
X(f'{i}<SessionMaxAge>{session_max_age}</SessionMaxAge>')
@@ -2000,7 +2111,7 @@ def emit_url_template(indent, tmpl_name, tmpl_def):
X(f'{indent}\t</Properties>')
if methods:
X(f'{indent}\t<ChildObjects>')
for method_name, http_method in methods.items():
for method_name, http_method in sorted(methods.items()):
method_uuid = new_uuid()
method_synonym = split_camel_case(method_name)
handler = f'{tmpl_name}{method_name}'
@@ -2051,7 +2162,7 @@ def emit_operation(indent, op_name, op_def):
X(f'{indent}\t</Properties>')
if params:
X(f'{indent}\t<ChildObjects>')
for param_name, param_def in params.items():
for param_name, param_def in sorted(params.items()):
param_uuid = new_uuid()
param_synonym = split_camel_case(param_name)
param_type = 'xs:string'
@@ -2123,6 +2234,27 @@ def emit_addressing_attribute(indent, addr_def):
xmlns_decl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# ---------------------------------------------------------------------------
# 14a. Detect format version from existing Configuration.xml
# ---------------------------------------------------------------------------
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"
format_version = detect_format_version(output_dir)
# ---------------------------------------------------------------------------
# 15. Main assembler
# ---------------------------------------------------------------------------
@@ -2130,7 +2262,7 @@ xmlns_decl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8
obj_uuid = new_uuid()
X('<?xml version="1.0" encoding="UTF-8"?>')
X(f'<MetaDataObject {xmlns_decl} version="2.17">')
X(f'<MetaDataObject {xmlns_decl} version="{format_version}">')
X(f'\t<{obj_type} uuid="{obj_uuid}">')
# InternalInfo
@@ -2228,6 +2360,8 @@ if obj_type in types_with_attr_ts:
context = 'document'
elif obj_type in ('DataProcessor', 'Report'):
context = 'processor'
elif obj_type in ('ChartOfAccounts', 'ChartOfCharacteristicTypes', 'ChartOfCalculationTypes'):
context = 'chart'
else:
context = 'object'
for a in attrs:
@@ -2236,9 +2370,11 @@ if obj_type in types_with_attr_ts:
columns = ts_sections[ts_name]
emit_tabular_section('\t\t\t', ts_name, columns, obj_type, obj_name)
for af in acct_flags:
emit_accounting_flag('\t\t\t', str(af))
af_name = af['name'] if isinstance(af, dict) else str(af)
emit_accounting_flag('\t\t\t', af_name)
for edf in ext_dim_flags:
emit_ext_dimension_accounting_flag('\t\t\t', str(edf))
edf_name = edf['name'] if isinstance(edf, dict) else str(edf)
emit_ext_dimension_accounting_flag('\t\t\t', edf_name)
for aa in addr_attrs:
emit_addressing_attribute('\t\t\t', aa)
X('\t\t</ChildObjects>')
@@ -2283,8 +2419,11 @@ if obj_type in ('InformationRegister', 'AccumulationRegister', 'AccountingRegist
emit_resource('\t\t\t', r, obj_type)
for d in dims:
emit_dimension('\t\t\t', d, obj_type)
# InformationRegister.Attribute supports FillFromFillingValue/FillValue/DataHistory;
# AccumulationRegister/AccountingRegister/CalculationRegister.Attribute do NOT.
reg_ctx = 'register-info' if obj_type == 'InformationRegister' else 'register-other'
for a in reg_attrs:
emit_attribute('\t\t\t', a, 'register')
emit_attribute('\t\t\t', a, reg_ctx)
X('\t\t</ChildObjects>')
else:
X('\t\t<ChildObjects/>')
@@ -2312,7 +2451,7 @@ if obj_type == 'HTTPService':
if url_templates:
has_children = True
X('\t\t<ChildObjects>')
for tmpl_name in url_tmpl_order:
for tmpl_name in sorted(url_tmpl_order):
emit_url_template('\t\t\t', tmpl_name, url_templates[tmpl_name])
X('\t\t</ChildObjects>')
else:
@@ -2329,7 +2468,7 @@ if obj_type == 'WebService':
if operations:
has_children = True
X('\t\t<ChildObjects>')
for op_name in op_order:
for op_name in sorted(op_order):
emit_operation('\t\t\t', op_name, operations[op_name])
X('\t\t</ChildObjects>')
else:
@@ -2340,7 +2479,7 @@ if obj_type == 'WebService':
X(f'\t</{obj_type}>')
X('</MetaDataObject>')
metadata_xml = '\n'.join(lines)
metadata_xml = '\n'.join(lines) + '\n'
# ---------------------------------------------------------------------------
# 16. Write files
@@ -2386,7 +2525,7 @@ ext_dir = os.path.join(obj_sub_dir, 'Ext')
os.makedirs(type_dir, exist_ok=True)
if obj_type not in types_no_sub_dir:
os.makedirs(ext_dir, exist_ok=True)
os.makedirs(obj_sub_dir, exist_ok=True)
write_utf8_bom(main_xml_path, metadata_xml)
@@ -2401,30 +2540,45 @@ types_with_object_module = [
types_with_record_set_module = [
'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister',
]
types_with_manager_module = ['Report', 'DataProcessor']
types_with_manager_module = ['Report', 'DataProcessor', 'Constant', 'Enum']
types_with_value_manager_module = ['Constant']
types_with_module = ['CommonModule', 'HTTPService', 'WebService']
def ensure_ext_dir():
os.makedirs(ext_dir, exist_ok=True)
if obj_type in types_with_object_module:
module_path = os.path.join(ext_dir, 'ObjectModule.bsl')
if not os.path.isfile(module_path):
ensure_ext_dir()
write_utf8_bom(module_path, '')
modules_created.append(module_path)
if obj_type in types_with_manager_module:
module_path = os.path.join(ext_dir, 'ManagerModule.bsl')
if not os.path.isfile(module_path):
ensure_ext_dir()
write_utf8_bom(module_path, '')
modules_created.append(module_path)
if obj_type in types_with_value_manager_module:
module_path = os.path.join(ext_dir, 'ValueManagerModule.bsl')
if not os.path.isfile(module_path):
ensure_ext_dir()
write_utf8_bom(module_path, '')
modules_created.append(module_path)
if obj_type in types_with_record_set_module:
module_path = os.path.join(ext_dir, 'RecordSetModule.bsl')
if not os.path.isfile(module_path):
ensure_ext_dir()
write_utf8_bom(module_path, '')
modules_created.append(module_path)
if obj_type in types_with_module:
module_path = os.path.join(ext_dir, 'Module.bsl')
if not os.path.isfile(module_path):
ensure_ext_dir()
write_utf8_bom(module_path, '')
modules_created.append(module_path)
@@ -2432,14 +2586,16 @@ if obj_type in types_with_module:
if obj_type == 'ExchangePlan':
content_path = os.path.join(ext_dir, 'Content.xml')
if not os.path.isfile(content_path):
content_xml = '<?xml version="1.0" encoding="UTF-8"?>\r\n<ExchangePlanContent xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" version="2.17"/>\r\n'
ensure_ext_dir()
content_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<ExchangePlanContent xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" version="{format_version}"/>\r\n'
write_utf8_bom(content_path, content_xml)
modules_created.append(content_path)
if obj_type == 'BusinessProcess':
flowchart_path = os.path.join(ext_dir, 'Flowchart.xml')
if not os.path.isfile(flowchart_path):
flowchart_xml = '<?xml version="1.0" encoding="UTF-8"?>\r\n<Flowchart xmlns="http://v8.1c.ru/8.3/MDClasses" version="2.17"/>\r\n'
ensure_ext_dir()
flowchart_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<Flowchart xmlns="http://v8.1c.ru/8.3/MDClasses" version="{format_version}"/>\r\n'
write_utf8_bom(flowchart_path, flowchart_xml)
modules_created.append(flowchart_path)
@@ -2502,9 +2658,13 @@ if os.path.isfile(config_xml_path):
# Write back preserving BOM
tree.write(config_xml_path, encoding='utf-8', xml_declaration=True)
# Re-read to add BOM
# Re-read to add BOM, fix declaration quotes, ensure trailing newline
with open(config_xml_path, 'r', encoding='utf-8') as f:
raw = f.read()
if raw.startswith("<?xml version='1.0' encoding='utf-8'?>"):
raw = raw.replace("<?xml version='1.0' encoding='utf-8'?>", '<?xml version="1.0" encoding="UTF-8"?>', 1)
if not raw.endswith('\n'):
raw += '\n'
write_utf8_bom(config_xml_path, raw)
reg_result = 'added'
else:
+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>"
```
| Параметр | Описание |
+73 -3
View File
@@ -1,9 +1,10 @@
# meta-edit v1.4 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# meta-edit v1.6 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ObjectPath,
# Inline mode (alternative to DefinitionFile)
@@ -43,6 +44,73 @@ if (-not $DefinitionFile -and -not $Operation) {
exit 1
}
# --- Enum value normalization (same as meta-compile) ---
$script:enumValueAliases = @{
"Balances" = "Balance"; "Остатки" = "Balance"; "Обороты" = "Turnovers"
"RecordSubordinate" = "RecorderSubordinate"; "Subordinate" = "RecorderSubordinate"
"ПодчинениеРегистратору" = "RecorderSubordinate"; "Независимый" = "Independent"
"NotDependOnCalculationTypes" = "DontUse"; "NoDependence" = "DontUse"; "NotUsed" = "DontUse"
"Depend" = "OnActionPeriod"; "ПоПериодуДействия" = "OnActionPeriod"
"None" = "Nonperiodical"; "Daily" = "Day"; "Monthly" = "Month"
"Quarterly" = "Quarter"; "Yearly" = "Year"
"Непериодический" = "Nonperiodical"; "Секунда" = "Second"; "День" = "Day"; "Месяц" = "Month"
"Квартал" = "Quarter"; "Год" = "Year"
"ПозицияРегистратора" = "RecorderPosition"
"Автоматический" = "Automatic"; "Управляемый" = "Managed"
"Использовать" = "Use"; "НеИспользовать" = "DontUse"
"Разрешить" = "Allow"; "Запретить" = "Deny"
"ВДиалоге" = "InDialog"; "ВСписке" = "InList"; "ОбаСпособа" = "BothWays"
"ВВидеНаименования" = "AsDescription"; "ВВидеКода" = "AsCode"
"НеПроверять" = "DontCheck"; "Ошибка" = "ShowError"; "Предупреждение" = "ShowWarning"
"НеИндексировать" = "DontIndex"; "Индексировать" = "Index"
"ИндексироватьСДопУпорядочиванием" = "IndexWithAdditionalOrder"
}
$script:validEnumValues = @{
"RegisterType" = @("Balance","Turnovers")
"WriteMode" = @("Independent","RecorderSubordinate")
"InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition")
"DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod")
"DataLockControlMode" = @("Automatic","Managed")
"FullTextSearch" = @("Use","DontUse")
"DataHistory" = @("Use","DontUse")
"DefaultPresentation" = @("AsDescription","AsCode")
"Posting" = @("Allow","Deny")
"RealTimePosting" = @("Allow","Deny")
"EditType" = @("InDialog","InList","BothWays")
"HierarchyType" = @("HierarchyFoldersAndItems","HierarchyItemsOnly")
"CodeType" = @("String","Number")
"CodeAllowedLength" = @("Variable","Fixed")
"NumberType" = @("String","Number")
"NumberAllowedLength" = @("Variable","Fixed")
"RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff")
"RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll")
"ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession")
"ReuseSessions" = @("DontUse","AutoUse")
"FillChecking" = @("DontCheck","ShowError","ShowWarning")
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
}
function Normalize-EnumValue {
param([string]$propName, [string]$value)
# 1. Check alias dictionary — silent auto-correct
if ($script:enumValueAliases.ContainsKey($value)) {
return $script:enumValueAliases[$value]
}
# 2. Case-insensitive match against valid values — silent
$valid = $script:validEnumValues[$propName]
if ($valid) {
foreach ($v in $valid) {
if ($v -ieq $value) { return $v }
}
# 3. Known property, unknown value — error with hint
Write-Error "Invalid value '$value' for property '$propName'. Valid values: $($valid -join ', ')"
exit 1
}
# 4. Unknown property — pass-through (no validation data)
return $value
}
# --- Load JSON definition (DefinitionFile mode) ---
$def = $null
if ($DefinitionFile) {
@@ -756,7 +824,7 @@ function Build-AttributeFragment {
# FillChecking
$fillChecking = "DontCheck"
if ($parsed.flags -contains "req") { $fillChecking = "ShowError" }
if ($parsed.fillChecking) { $fillChecking = $parsed.fillChecking }
if ($parsed.fillChecking) { $fillChecking = Normalize-EnumValue "FillChecking" $parsed.fillChecking }
$sb.AppendLine("$indent`t`t<FillChecking>$fillChecking</FillChecking>") | Out-Null
$sb.AppendLine("$indent`t`t<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>") | Out-Null
@@ -778,7 +846,7 @@ function Build-AttributeFragment {
$indexing = "DontIndex"
if ($parsed.flags -contains "index") { $indexing = "Index" }
if ($parsed.flags -contains "indexadditional") { $indexing = "IndexWithAdditionalOrder" }
if ($parsed.indexing) { $indexing = $parsed.indexing }
if ($parsed.indexing) { $indexing = Normalize-EnumValue "Indexing" $parsed.indexing }
$sb.AppendLine("$indent`t`t<Indexing>$indexing</Indexing>") | Out-Null
$sb.AppendLine("$indent`t`t<FullTextSearch>Use</FullTextSearch>") | Out-Null
@@ -2040,6 +2108,8 @@ function Modify-ChildElements($modifyDef, [string]$childType) {
$valueStr = "$changeValue"
if ($changeValue -is [bool]) {
$valueStr = if ($changeValue) { "true" } else { "false" }
} else {
$valueStr = Normalize-EnumValue $changeProp $valueStr
}
$scalarEl.InnerText = $valueStr
Info "Modified $xmlTag '$elemName'.$changeProp = $valueStr"
+91 -8
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-edit v1.4 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# meta-edit v1.6 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -77,6 +77,85 @@ def esc_xml(s):
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
# ============================================================
# Enum value normalization (same as meta-compile)
# ============================================================
enum_value_aliases = {
# RegisterType (AccumulationRegister)
'Balances': 'Balance', 'Остатки': 'Balance', 'Обороты': 'Turnovers',
# WriteMode (InformationRegister)
'RecordSubordinate': 'RecorderSubordinate', 'Subordinate': 'RecorderSubordinate',
'ПодчинениеРегистратору': 'RecorderSubordinate', 'Независимый': 'Independent',
# DependenceOnCalculationTypes (ChartOfCalculationTypes)
'NotDependOnCalculationTypes': 'DontUse', 'NoDependence': 'DontUse', 'NotUsed': 'DontUse',
'Depend': 'OnActionPeriod', 'ПоПериодуДействия': 'OnActionPeriod',
# InformationRegisterPeriodicity
'None': 'Nonperiodical', 'Daily': 'Day', 'Monthly': 'Month',
'Quarterly': 'Quarter', 'Yearly': 'Year',
'Непериодический': 'Nonperiodical', 'Секунда': 'Second', 'День': 'Day',
'Месяц': 'Month', 'Квартал': 'Quarter', 'Год': 'Year',
'ПозицияРегистратора': 'RecorderPosition',
# DataLockControlMode
'Автоматический': 'Automatic', 'Управляемый': 'Managed',
# FullTextSearch
'Использовать': 'Use', 'НеИспользовать': 'DontUse',
# Posting
'Разрешить': 'Allow', 'Запретить': 'Deny',
# EditType
'ВДиалоге': 'InDialog', 'ВСписке': 'InList', 'ОбаСпособа': 'BothWays',
# DefaultPresentation
'ВВидеНаименования': 'AsDescription', 'ВВидеКода': 'AsCode',
# FillChecking
'НеПроверять': 'DontCheck', 'Ошибка': 'ShowError', 'Предупреждение': 'ShowWarning',
# Indexing
'НеИндексировать': 'DontIndex', 'Индексировать': 'Index',
'ИндексироватьСДопУпорядочиванием': 'IndexWithAdditionalOrder',
}
valid_enum_values = {
'RegisterType': ['Balance', 'Turnovers'],
'WriteMode': ['Independent', 'RecorderSubordinate'],
'InformationRegisterPeriodicity': ['Nonperiodical', 'Second', 'Day', 'Month', 'Quarter', 'Year', 'RecorderPosition'],
'DependenceOnCalculationTypes': ['DontUse', 'OnActionPeriod'],
'DataLockControlMode': ['Automatic', 'Managed'],
'FullTextSearch': ['Use', 'DontUse'],
'DataHistory': ['Use', 'DontUse'],
'DefaultPresentation': ['AsDescription', 'AsCode'],
'Posting': ['Allow', 'Deny'],
'RealTimePosting': ['Allow', 'Deny'],
'EditType': ['InDialog', 'InList', 'BothWays'],
'HierarchyType': ['HierarchyFoldersAndItems', 'HierarchyItemsOnly'],
'CodeType': ['String', 'Number'],
'CodeAllowedLength': ['Variable', 'Fixed'],
'NumberType': ['String', 'Number'],
'NumberAllowedLength': ['Variable', 'Fixed'],
'RegisterRecordsDeletion': ['AutoDelete', 'AutoDeleteOnUnpost', 'AutoDeleteOff'],
'RegisterRecordsWritingOnPost': ['WriteModified', 'WriteSelected', 'WriteAll'],
'ReturnValuesReuse': ['DontUse', 'DuringRequest', 'DuringSession'],
'ReuseSessions': ['DontUse', 'AutoUse'],
'FillChecking': ['DontCheck', 'ShowError', 'ShowWarning'],
'Indexing': ['DontIndex', 'Index', 'IndexWithAdditionalOrder'],
}
def normalize_enum_value(prop_name, value):
# 1. Check alias dictionary — silent auto-correct
if value in enum_value_aliases:
return enum_value_aliases[value]
# 2. Case-insensitive match against valid values — silent
valid = valid_enum_values.get(prop_name)
if valid:
for v in valid:
if v.lower() == value.lower():
return v
# 3. Known property, unknown value — error with hint
print(f"Invalid value '{value}' for property '{prop_name}'. Valid values: {', '.join(valid)}", file=sys.stderr)
sys.exit(1)
# 4. Unknown property — pass-through (no validation data)
return value
def new_uuid():
return str(uuid.uuid4())
@@ -552,8 +631,8 @@ def parse_attribute_shorthand(val):
"synonym": str(val.get("synonym", "")) if val.get("synonym") else split_camel_case(name),
"comment": str(val.get("comment", "")),
"flags": list(val.get("flags", [])),
"fillChecking": str(val.get("fillChecking", "")),
"indexing": str(val.get("indexing", "")),
"fillChecking": normalize_enum_value("FillChecking", str(val.get("fillChecking", ""))) if val.get("fillChecking") else "",
"indexing": normalize_enum_value("Indexing", str(val.get("indexing", ""))) if val.get("indexing") else "",
"after": str(val.get("after", "")),
"before": str(val.get("before", "")),
}
@@ -986,7 +1065,7 @@ def build_column_fragment(col_def, indent):
name = str(col_def.get("name", ""))
synonym = str(col_def.get("synonym", "")) if col_def.get("synonym") else split_camel_case(name)
if col_def.get("indexing"):
indexing = str(col_def["indexing"])
indexing = normalize_enum_value("Indexing", str(col_def["indexing"]))
if col_def.get("references"):
references = list(col_def["references"])
@@ -1823,6 +1902,8 @@ def modify_child_elements(modify_def, child_type):
value_str = str(change_value)
if isinstance(change_value, bool):
value_str = "true" if change_value else "false"
else:
value_str = normalize_enum_value(change_prop, value_str)
# Clear children and set text
for ch in list(scalar_el):
scalar_el.remove(ch)
@@ -1997,8 +2078,8 @@ def set_complex_property(property_name, values):
def save_xml(tree, path):
"""Save XML tree with BOM and proper encoding declaration."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
# Fix encoding quotes: encoding='UTF-8' -> encoding="UTF-8"
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
# Fix XML declaration quotes
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
# Fix d5p1 namespace declarations stripped by lxml (it treats them as unused
# because d5p1: appears only in text content, not in element/attribute names)
xml_bytes = re.sub(
@@ -2006,6 +2087,8 @@ def save_xml(tree, path):
b'\\1 xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config"\\2',
xml_bytes
)
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)
@@ -2038,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")
@@ -2174,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 "<путь>"
```
## Три режима

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