diff --git a/.claude/skills/meta-edit/SKILL.md b/.claude/skills/meta-edit/SKILL.md
index d03a4f06c..61929097d 100644
--- a/.claude/skills/meta-edit/SKILL.md
+++ b/.claude/skills/meta-edit/SKILL.md
@@ -49,7 +49,7 @@ Batch через `;;` во всех операциях. Подробный си
| `add-resource` | `Имя: Тип` | `"Сумма: Число(15,2)"` |
| `add-enumValue` | `Имя` | `"Значение1 ;; Значение2"` |
| `add-column` | `Имя: Тип` | `"Тип: EnumRef.ТипыДокументов"` |
-| `add-form` / `add-template` / `add-command` | `Имя` | `"ФормаЭлемента"` |
+| `add-command` | `Имя` | `"ОткрытьДосье"` |
| `add-ts-attribute` | `ТЧ.Имя: Тип` | `"Товары.Скидка: Число(15,2)"` |
| `remove-*` | `Имя` | `"СтарыйРеквизит ;; ЕщёОдин"` |
| `remove-ts-attribute` | `ТЧ.Имя` | `"Товары.УстаревшийРекв"` |
@@ -59,6 +59,9 @@ Batch через `;;` во всех операциях. Подробный си
Позиционная вставка: `"Склад: CatalogRef.Склады >> after Организация"`.
+Форма — `form-add` / `form-remove`, макет — `template-add` / `template-remove`: кроме записи в `ChildObjects`
+у них есть собственные файлы, и `meta-edit` их не трогает.
+
`modify-attribute` умеет и структурные свойства реквизита — формат/подсказку, форму и параметры выбора,
значение заполнения, границы (`Format`, `ChoiceForm`, `ChoiceParameters`, `FillValue`, `MinValue`/`MaxValue` и др.).
diff --git a/.claude/skills/meta-edit/child-operations.md b/.claude/skills/meta-edit/child-operations.md
index 8857cb582..347fc948f 100644
--- a/.claude/skills/meta-edit/child-operations.md
+++ b/.claude/skills/meta-edit/child-operations.md
@@ -133,16 +133,19 @@ JSON — строки и/или объекты (для групп с вложе
Ключи объекта: `name`, `code`, `description`, `isFolder`, `childItems` (дерево). Тип кода (строковый/числовой)
берётся из объекта автоматически.
-## add-enumValue / add-form / add-template / add-command
+## add-enumValue / add-command
Просто имена (batch через `;;`):
```powershell
-Operation add-enumValue -Value "Значение1 ;; Значение2 ;; Значение3"
--Operation add-form -Value "ФормаЭлемента ;; ФормаСписка"
--Operation add-template -Value "ПечатнаяФорма"
-Operation add-command -Value "Команда1"
```
+Команда создаётся вместе с заготовкой модуля `Commands/<Имя>/Ext/CommandModule.bsl`.
+
+Формы и макеты этот навык не добавляет и не удаляет — у них есть собственные файлы:
+`form-add` / `form-remove` и `template-add` / `template-remove`.
+
## remove-*
Имя элемента (или несколько через `;;`):
diff --git a/.claude/skills/meta-edit/json-dsl.md b/.claude/skills/meta-edit/json-dsl.md
index 439425226..e4edcac40 100644
--- a/.claude/skills/meta-edit/json-dsl.md
+++ b/.claude/skills/meta-edit/json-dsl.md
@@ -22,8 +22,7 @@ powershell.exe -NoProfile -File ${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1 -Defin
{ "name": "Количество", "type": "Число(15,3)" }
]
}],
- "forms": ["ФормаЭлемента"],
- "templates": ["ПечатнаяФорма"]
+ "commands": ["Пересчитать"]
}
}
```
@@ -120,6 +119,9 @@ powershell.exe -NoProfile -File ${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1 -Defin
| commands | команды |
| properties | свойства |
+Формы и макеты `meta-edit` не добавляет и не удаляет: кроме записи в `ChildObjects` у них есть собственные файлы.
+Форма — `form-add` / `form-remove`, макет — `template-add` / `template-remove`. Команда файла не имеет — её `meta-edit` добавляет сам.
+
## Составные типы
Для полей с несколькими допустимыми типами — массив в `type`:
@@ -141,8 +143,11 @@ powershell.exe -NoProfile -File ${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1 -Defin
| Тип объекта | Допустимые add-типы |
|-------------|-------------------|
-| Catalog, Document, ExchangePlan, ChartOf*, BP, Task, Report, DP | attributes, tabularSections, forms, templates, commands |
-| Enum | enumValues, forms, templates, commands |
-| *Register (4 типа) | dimensions, resources, attributes, forms, templates, commands |
-| DocumentJournal | columns, forms, templates, commands |
-| Constant | forms |
+| Catalog, Document, ExchangePlan, ChartOf*, BP, Task, Report, DP | attributes, tabularSections, commands |
+| Enum | enumValues, commands |
+| *Register (4 типа) | dimensions, resources, attributes, commands |
+| DocumentJournal | columns, commands |
+| ExternalDataSource | tables, functions |
+| Table (таблица внешнего источника) | fields, commands |
+
+У `Constant` добавлять нечего: единственный её ребёнок — форма, а форму добавляет `form-add`.
diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1
index b67175e21..0ef68623c 100644
--- a/.claude/skills/meta-edit/scripts/meta-edit.ps1
+++ b/.claude/skills/meta-edit/scripts/meta-edit.ps1
@@ -1,4 +1,4 @@
-# meta-edit v1.47 — Edit existing 1C metadata object XML
+# meta-edit v1.48 — Edit existing 1C metadata object XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
@@ -904,6 +904,9 @@ function Collapse-ChildObjectsIfEmpty {
while ($script:childObjectsEl.HasChildNodes) {
$script:childObjectsEl.RemoveChild($script:childObjectsEl.FirstChild) | Out-Null
}
+ # XmlDocument помнит, что у узла были дети, и пишет .
+ # Платформа и py-порт дают — сбрасываем флаг явно.
+ $script:childObjectsEl.IsEmpty = $true
}
}
@@ -1458,35 +1461,29 @@ function Build-ColumnFragment {
return $sb.ToString()
}
-function Build-SimpleChildFragment {
- param([string]$tagName, [string]$name, [string]$indent)
- # For Form, Template, Command — just a name wrapper
+function Build-CommandFragment {
+ param([string]$name, [string]$indent)
+ # Команда объекта описывается целиком внутри ChildObjects (отдельного файла у неё нет).
+ # Порядок свойств платформенный — переставлять нельзя.
$uuid = New-Guid-String
$synonym = Split-CamelCase $name
$sb = New-Object System.Text.StringBuilder
- $sb.AppendLine("$indent<$tagName uuid=`"$uuid`">") | Out-Null
+ $sb.AppendLine("$indent") | Out-Null
$sb.AppendLine("$indent`t") | Out-Null
$sb.AppendLine("$indent`t`t$(Esc-XmlText $name)") | Out-Null
$sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $synonym)) | Out-Null
$sb.AppendLine("$indent`t`t") | Out-Null
- # Forms get additional properties
- if ($tagName -eq "Form") {
- $sb.AppendLine("$indent`t`tOrdinary") | Out-Null
- $sb.AppendLine("$indent`t`tfalse") | Out-Null
- $sb.AppendLine("$indent`t`t") | Out-Null
- }
- if ($tagName -eq "Template") {
- $sb.AppendLine("$indent`t`tSpreadsheetDocument") | Out-Null
- }
- if ($tagName -eq "Command") {
- $sb.AppendLine("$indent`t`tFormNavigationPanelGoTo") | Out-Null
- $sb.AppendLine("$indent`t`tAuto") | Out-Null
- $sb.AppendLine("$indent`t`t") | Out-Null
- $sb.AppendLine("$indent`t`t") | Out-Null
- $sb.AppendLine("$indent`t`t") | Out-Null
- }
+ $sb.AppendLine("$indent`t`tFormNavigationPanelGoTo") | Out-Null
+ $sb.AppendLine("$indent`t`t") | Out-Null
+ $sb.AppendLine("$indent`t`tSingle") | Out-Null
+ $sb.AppendLine("$indent`t`tfalse") | Out-Null
+ $sb.AppendLine("$indent`t`tAuto") | Out-Null
+ $sb.AppendLine("$indent`t`t") | Out-Null
+ $sb.AppendLine("$indent`t`t") | Out-Null
+ $sb.AppendLine("$indent`t`t") | Out-Null
+ $sb.AppendLine("$indent`t`tAuto") | Out-Null
$sb.AppendLine("$indent`t") | Out-Null
- $sb.Append("$indent$tagName>") | Out-Null
+ $sb.Append("$indent") | Out-Null
return $sb.ToString()
}
@@ -1576,7 +1573,7 @@ $script:validChildTypes = @{
# Canonical child order in ChildObjects
$script:childOrder = @(
- "Resource", "Dimension", "Attribute", "TabularSection", "Field", "Function",
+ "Resource", "Dimension", "Attribute", "TabularSection", "Field", "Table", "Function",
"AccountingFlag", "ExtDimensionAccountingFlag",
"EnumValue", "Column", "AddressingAttribute", "Recalculation",
"Form", "Template", "Command"
@@ -2421,24 +2418,36 @@ function Process-Add($addDef) {
$existingNames[$colName] = "Column"
}
}
- { $_ -in @("forms","templates","commands") } {
- $tagMap = @{ "forms" = "Form"; "templates" = "Template"; "commands" = "Command" }
- $tag = $tagMap[$childType]
+ { $_ -in @("forms","templates") } {
+ # Форма и макет регистрируются голым текстом (
) и требуют ещё и
+ # собственных файлов. И то и другое делают form-add / template-add — дублировать
+ # эту ответственность здесь нельзя: получится висячая регистрация без файла.
+ $skillName = if ($childType -eq "forms") { "form-add" } else { "template-add" }
+ $whatName = if ($childType -eq "forms") { "Форму" } else { "Макет" }
+ Warn "$whatName добавляет навык $skillName (он создаёт и файл, и запись в ChildObjects). meta-edit этого не делает — операция пропущена."
+ }
+ "commands" {
foreach ($item in $items) {
$itemName = if ($item -is [string]) { "$item" } else { "$($item.name)" }
if ($existingNames.ContainsKey($itemName)) {
- Warn "$tag '$itemName' already exists, skipping"
+ Warn "Command '$itemName' already exists, skipping"
continue
}
- $fragmentXml = Build-SimpleChildFragment $tag $itemName $indent
+ # У команды есть модуль обработчика (Commands/<Имя>/Ext/CommandModule.bsl) — в корпусе
+ # он есть у всех команд без исключения. Пишем ту же заготовку, что и meta-compile.
+ $cmdExtDir = Join-Path (Join-Path (Join-Path (Join-Path (Split-Path -Parent $resolvedPath) $script:objName) "Commands") $itemName) "Ext"
+ $cmdModPath = Join-Path $cmdExtDir "CommandModule.bsl"
+ if (-not (Test-Path $cmdExtDir)) { New-Item -ItemType Directory -Path $cmdExtDir -Force | Out-Null }
+ [System.IO.File]::WriteAllText($cmdModPath, "&НаКлиенте`r`nПроцедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)`r`n`r`n`t// Вставьте обработчик команды.`r`n`r`nКонецПроцедуры`r`n", (New-Object System.Text.UTF8Encoding($true)))
+ $fragmentXml = Build-CommandFragment $itemName $indent
$nodes = Import-Fragment $fragmentXml
- $refNode = Find-InsertionPoint $tag @{ after = ""; before = "" }
+ $refNode = Find-InsertionPoint "Command" @{ after = ""; before = "" }
foreach ($node in $nodes) {
Insert-BeforeElement $script:childObjectsEl $node $refNode $indent
}
- Info "Added $($tag.ToLower()): $itemName"
+ Info "Added command: $itemName ($cmdModPath)"
$script:addCount++
- $existingNames[$itemName] = $tag
+ $existingNames[$itemName] = "Command"
}
}
}
@@ -2463,6 +2472,13 @@ function Process-Remove($removeDef) {
Warn "Cannot remove properties — use modify instead"
return
}
+ if ($childType -in @("forms","templates")) {
+ # Снять регистрацию мало — надо удалить и файлы; это делают form-remove / template-remove.
+ $skillName = if ($childType -eq "forms") { "form-remove" } else { "template-remove" }
+ $whatName = if ($childType -eq "forms") { "Форму" } else { "Макет" }
+ Warn "$whatName удаляет навык $skillName (он убирает и файлы, и запись в ChildObjects). meta-edit этого не делает — операция пропущена."
+ return
+ }
$xmlTag = $script:childTypeToXmlTag[$childType]
if (-not $xmlTag -or -not $script:childObjectsEl) {
diff --git a/.claude/skills/meta-edit/scripts/meta-edit.py b/.claude/skills/meta-edit/scripts/meta-edit.py
index 7fcfb38f2..f7498ec08 100644
--- a/.claude/skills/meta-edit/scripts/meta-edit.py
+++ b/.claude/skills/meta-edit/scripts/meta-edit.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# meta-edit v1.47 — Edit existing 1C metadata object XML
+# meta-edit v1.48 — Edit existing 1C metadata object XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -1478,34 +1478,32 @@ def build_column_fragment(col_def, indent):
return "\r\n".join(lines)
-def build_simple_child_fragment(tag_name, name, indent):
- """Build XML fragment for Form, Template, Command -- just a name wrapper."""
+def build_command_fragment(name, indent):
+ """Build XML fragment for a Command (it has no separate file -- all inline).
+
+ Property order is the platform's -- do not reorder.
+ """
uid = new_uuid()
synonym = split_camel_case(name)
lines = []
- lines.append(f'{indent}<{tag_name} uuid="{uid}">')
+ lines.append(f'{indent}')
lines.append(f"{indent}\t")
lines.append(f"{indent}\t\t{esc_xml_text(name)}")
lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", synonym))
lines.append(f"{indent}\t\t")
- # Forms get additional properties
- if tag_name == "Form":
- lines.append(f"{indent}\t\tOrdinary")
- lines.append(f"{indent}\t\tfalse")
- lines.append(f"{indent}\t\t")
- if tag_name == "Template":
- lines.append(f"{indent}\t\tSpreadsheetDocument")
- if tag_name == "Command":
- lines.append(f"{indent}\t\tFormNavigationPanelGoTo")
- lines.append(f"{indent}\t\tAuto")
- lines.append(f"{indent}\t\t")
- lines.append(f"{indent}\t\t")
- lines.append(f"{indent}\t\t")
+ lines.append(f"{indent}\t\tFormNavigationPanelGoTo")
+ lines.append(f"{indent}\t\t")
+ lines.append(f"{indent}\t\tSingle")
+ lines.append(f"{indent}\t\tfalse")
+ lines.append(f"{indent}\t\tAuto")
+ lines.append(f"{indent}\t\t")
+ lines.append(f"{indent}\t\t")
+ lines.append(f"{indent}\t\t")
+ lines.append(f"{indent}\t\tAuto")
lines.append(f"{indent}\t")
- lines.append(f"{indent}{tag_name}>")
+ lines.append(f"{indent}")
return "\r\n".join(lines)
-
# ============================================================
# Name uniqueness check
# ============================================================
@@ -1567,7 +1565,7 @@ valid_child_types = {
# Canonical child order in ChildObjects
child_order = [
- "Resource", "Dimension", "Attribute", "TabularSection", "Field", "Function",
+ "Resource", "Dimension", "Attribute", "TabularSection", "Field", "Table", "Function",
"AccountingFlag", "ExtDimensionAccountingFlag",
"EnumValue", "Column", "AddressingAttribute", "Recalculation",
"Form", "Template", "Command",
@@ -2350,26 +2348,42 @@ def process_add(add_def):
add_count += 1
existing_names[col_name] = "Column"
- elif child_type in ("forms", "templates", "commands"):
- tag_map = {"forms": "Form", "templates": "Template", "commands": "Command"}
- tag = tag_map[child_type]
+ elif child_type in ("forms", "templates"):
+ # Форма и макет регистрируются голым текстом () и требуют ещё и
+ # собственных файлов. И то и другое делают form-add / template-add -- дублировать
+ # эту ответственность здесь нельзя: получится висячая регистрация без файла.
+ skill_name = "form-add" if child_type == "forms" else "template-add"
+ what_name = "Форму" if child_type == "forms" else "Макет"
+ warn(
+ f"{what_name} добавляет навык {skill_name} "
+ "(он создаёт и файл, и запись в ChildObjects). "
+ "meta-edit этого не делает — операция пропущена."
+ )
+
+ elif child_type == "commands":
for item in items:
if isinstance(item, str):
item_name = item
else:
item_name = str(item.get("name", ""))
if item_name in existing_names:
- warn(f"{tag} '{item_name}' already exists, skipping")
+ warn(f"Command '{item_name}' already exists, skipping")
continue
- fragment_xml = build_simple_child_fragment(tag, item_name, indent)
+ # У команды есть модуль обработчика (Commands/<Имя>/Ext/CommandModule.bsl) — в корпусе
+ # он есть у всех команд без исключения. Пишем ту же заготовку, что и meta-compile.
+ cmd_ext_dir = os.path.join(os.path.dirname(resolved_path), obj_name, "Commands", item_name, "Ext")
+ cmd_mod_path = os.path.join(cmd_ext_dir, "CommandModule.bsl")
+ os.makedirs(cmd_ext_dir, exist_ok=True)
+ with open(cmd_mod_path, "w", encoding="utf-8-sig", newline="") as fh:
+ fh.write("&НаКлиенте\r\nПроцедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)\r\n\r\n\t// Вставьте обработчик команды.\r\n\r\nКонецПроцедуры\r\n")
+ fragment_xml = build_command_fragment(item_name, indent)
nodes = import_fragment(fragment_xml)
- ref_node = find_insertion_point(tag, {"after": "", "before": ""})
+ ref_node = find_insertion_point("Command", {"after": "", "before": ""})
for node in nodes:
insert_before_element(child_objects_el, node, ref_node, indent)
- info(f"Added {tag.lower()}: {item_name}")
+ info(f"Added command: {item_name} ({cmd_mod_path})")
add_count += 1
- existing_names[item_name] = tag
-
+ existing_names[item_name] = "Command"
# ============================================================
# REMOVE operations
@@ -2388,6 +2402,16 @@ def process_remove(remove_def):
if child_type == "properties":
warn("Cannot remove properties -- use modify instead")
continue
+ if child_type in ("forms", "templates"):
+ # Снять регистрацию мало — надо удалить и файлы; это делают form-remove / template-remove.
+ skill_name = "form-remove" if child_type == "forms" else "template-remove"
+ what_name = "Форму" if child_type == "forms" else "Макет"
+ warn(
+ f"{what_name} удаляет навык {skill_name} "
+ "(он убирает и файлы, и запись в ChildObjects). "
+ "meta-edit этого не делает — операция пропущена."
+ )
+ continue
xml_tag = child_type_to_xml_tag.get(child_type)
if not xml_tag or child_objects_el is None:
diff --git a/tests/skills/cases/meta-edit/add-command.json b/tests/skills/cases/meta-edit/add-command.json
new file mode 100644
index 000000000..c2e81683e
--- /dev/null
+++ b/tests/skills/cases/meta-edit/add-command.json
@@ -0,0 +1,45 @@
+{
+ "name": "Команда объекта добавляется целиком (файла у неё нет)",
+ "preRun": [
+ {
+ "script": "meta-compile/scripts/meta-compile",
+ "input": {
+ "type": "Catalog",
+ "name": "Контрагенты"
+ },
+ "args": {
+ "-JsonPath": "{inputFile}",
+ "-OutputDir": "{workDir}"
+ }
+ }
+ ],
+ "params": {
+ "objectPath": "Catalogs/Контрагенты"
+ },
+ "input": {
+ "add": {
+ "commands": [
+ "ОткрытьДосье"
+ ]
+ }
+ },
+ "expect": {
+ "stdoutContains": [
+ "Added command: ОткрытьДосье"
+ ],
+ "fileContains": [
+ {
+ "file": "Catalogs/Контрагенты.xml",
+ "text": [
+ "Single",
+ "false",
+ "Auto"
+ ]
+ }
+ ],
+ "files": [
+ "Catalogs/Контрагенты/Commands/ОткрытьДосье/Ext/CommandModule.bsl"
+ ]
+ }
+}
diff --git a/tests/skills/cases/meta-edit/forms-templates-delegated.json b/tests/skills/cases/meta-edit/forms-templates-delegated.json
new file mode 100644
index 000000000..3de97225b
--- /dev/null
+++ b/tests/skills/cases/meta-edit/forms-templates-delegated.json
@@ -0,0 +1,24 @@
+{
+ "name": "Формы и макеты meta-edit не регистрирует — отсылает к form-add/template-add",
+ "preRun": [
+ {
+ "script": "meta-compile/scripts/meta-compile",
+ "input": { "type": "Catalog", "name": "Контрагенты" },
+ "args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" }
+ }
+ ],
+ "params": { "objectPath": "Catalogs/Контрагенты" },
+ "input": {
+ "add": { "forms": ["ФормаЭлемента"], "templates": ["ПечатнаяФорма"] },
+ "remove": { "forms": ["ФормаСписка"] }
+ },
+ "expect": {
+ "stdoutContains": ["form-add", "template-add", "form-remove"],
+ "fileNotContains": [
+ {
+ "file": "Catalogs/Контрагенты.xml",
+ "text": ["