From 9c1985b710996bd002c47c9b1670c887977d027e Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 21 Feb 2026 14:23:33 +0300 Subject: [PATCH 1/5] docs(cfe): expand section 5.4 with borrowed form structure (BaseForm, callType, IDs) Previously section 5.4 only documented the metadata XML of borrowed forms. Now covers: two-part Form.xml structure (result + BaseForm), callType attribute (Before/After/Override) on events and commands, ID numbering convention (1000000+), own forms vs borrowed forms distinction, and form module patterns. Co-Authored-By: Claude Opus 4.6 --- docs/1c-extension-spec.md | 206 +++++++++++++++++++++++++++++++++++++- 1 file changed, 203 insertions(+), 3 deletions(-) diff --git a/docs/1c-extension-spec.md b/docs/1c-extension-spec.md index 85f9e4f6..43c00156 100644 --- a/docs/1c-extension-spec.md +++ b/docs/1c-extension-spec.md @@ -324,9 +324,20 @@ Enums/ # Перечисления ``` -### 5.4. Заимствованные формы +### 5.4. Формы в расширениях -Метаданные формы (файл `.xml` в каталоге `Forms/`): +В расширении существуют **два принципиально разных сценария** работы с формами: + +| Сценарий | Описание | `` | ID элементов | `callType` | +|----------|----------|:------------:|:------------:|:----------:| +| **Собственная форма** на заимствованном объекте | Новая форма, не существующая в базовой конфигурации | Нет | Обычные (1+) | Нет | +| **Заимствованная форма** | Расширение существующей формы базовой конфигурации | Есть | Базовые + 1000000+ | Есть | + +> **Как отличить:** Если файл метаданных формы (`.xml`) содержит `Adopted` — это заимствованная форма. Собственные формы не имеют `ObjectBelonging`. + +#### 5.4.1. Метаданные заимствованной формы + +Файл `.xml` в каталоге `Forms/`: ```xml
@@ -341,7 +352,196 @@ Enums/ # Перечисления
``` -Содержимое формы (расширение) хранится в `Forms/ФормаСписка/Ext/Form.xml`, модуль формы — в `Forms/ФормаСписка/Ext/Form/Module.bsl`. +Содержимое формы хранится в `Forms/ФормаСписка/Ext/Form.xml`, модуль формы — в `Forms/ФормаСписка/Ext/Form/Module.bsl`. + +#### 5.4.2. Структура Form.xml заимствованной формы + +Form.xml заимствованной формы — **двухчастный файл**: + +```xml +
+ + + + + + ... + + + + + + Расш1_ПриСозданииПосле + + + + + + ... + + + + Расш1_НоваяКомандаВместо + + + + + + ... + + + + + ... + + +
+``` + +**Ключевые правила:** + +1. **Часть 1** (до ``) — **результирующая форма**, содержит ВСЕ элементы: и базовые, и добавленные расширением. Именно эта часть определяет итоговую форму при запуске. + +2. **Часть 2** (``) — **полная копия оригинальной формы** из базовой конфигурации. Используется платформой для контроля совместимости — при обновлении конфигурации платформа сравнивает `` с текущей формой конфигурации и предупреждает о расхождениях. + +3. Элемент `` всегда идёт **последним** в `
` и имеет атрибут `version`. + +#### 5.4.3. Нумерация ID элементов + +| Диапазон | Принадлежность | +|----------|---------------| +| `-1` | Авто-командная панель (`AutoCommandBar`) — фиксированный ID | +| `1` – `999999` | Элементы базовой формы (сохраняют оригинальные ID) | +| `1000000`+ | Реквизиты (`Attributes`) и команды (`Commands`), добавленные расширением | + +> **Важно:** Визуальные элементы форм (элементы в `ChildItems`), добавленные расширением в тело базовой формы, могут использовать ID из обычного диапазона (продолжая нумерацию базовой формы). Диапазон 1000000+ гарантирован для `Attributes` и `Commands`. + +#### 5.4.4. Атрибут callType — перехват событий и команд + +В заимствованных формах события и действия команд используют атрибут `callType` для определения момента перехвата: + +| Значение | Описание | +|----------|----------| +| `Before` | Обработчик расширения вызывается **до** оригинального обработчика | +| `After` | Обработчик расширения вызывается **после** оригинального обработчика | +| `Override` | Обработчик расширения **заменяет** оригинальный обработчик | + +##### События формы (form-level) + +```xml + + Расш1_ПриСозданииНаСервереПосле + Расш1_ПриОткрытииПеред + Расш1_ПередЗаписьюНаСервереПосле + Расш1_ОбработкаОповещенияПосле + +``` + +##### События элементов формы (element-level) + +```xml + + + Расш1_БанкПриИзменении + Расш1_БанкОчистка + + ... + + + + + Расш1_СписокВыборПеред + + ... +
+``` + +##### Действия команд (Command Action) + +Команда может иметь **несколько элементов ``** с разными `callType`: + +```xml + + + Расш1_ПодборИзКлассификатораПеред + Расш1_ПодборИзКлассификатораПосле + + + + + Расш1_НоваяКомандаВместо + + + + + Расш1_ЗапросКорректировкиПосле + +``` + +> **Отличие от обычной формы:** В обычной форме (конфигурации или собственной форме расширения) у `` и `` **нет** атрибута `callType` — обработчик вызывается напрямую. + +#### 5.4.5. Собственная форма на заимствованном объекте + +Расширение может добавить к заимствованному объекту **собственную форму**, не существующую в базовой конфигурации. Такая форма: + +- **Не имеет** `ObjectBelonging` и `ExtendedConfigurationObject` в метаданных формы +- **Не содержит** `` в Form.xml +- **Не использует** атрибут `callType` +- Использует обычную нумерацию ID (1+) +- Формат полностью совпадает с форматом форм конфигурации (см. [1c-form-spec.md](1c-form-spec.md)) + +```xml + + + + Расш1_МояФорма + ... + + Managed + ... + + +``` + +```xml + +
+ + ПриСозданииНаСервере + + ... + ... +
+``` + +#### 5.4.6. Модуль заимствованной формы + +Модуль формы (`Forms/Имя/Ext/Form/Module.bsl`) в заимствованной форме использует те же декораторы перехвата, что и другие модули расширений (см. раздел 7.2): + +```bsl +&НаСервере +&Вместо("ЗаполнитьПодменюПараметры") +Процедура Расш1_ЗаполнитьПодменюПараметры() + ПродолжитьВызов(); +КонецПроцедуры + +&НаКлиенте +&ИзменениеИКонтроль("ПараметрыНаЯзыке") +Функция Расш1_ПараметрыНаЯзыке(КодЯзыка) + // ... тело с #Вставка / #Удаление маркерами ... +КонецФункции + +// Обработчик собственной команды расширения (без декоратора) +&НаКлиенте +Процедура Расш1_НоваяКомандаВместо(Команда) + // ... +КонецПроцедуры +``` + +> **Обработчики событий с `callType`** (определённые в Form.xml секции Events/Action) реализуются в модуле как обычные процедуры **без** аннотаций-декораторов — привязка к событию уже задана в XML через `callType`. --- From 28b967f59182dd69df2a44fc70e978663f3dbae7 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 21 Feb 2026 14:59:16 +0300 Subject: [PATCH 2/5] feat(cfe,form): add borrowed form support across 6 skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cfe-borrow: borrow forms via Type.Name.Form.FormName, auto-borrow parent, generate Form.xml with BaseForm + metadata + empty Module.bsl - form-edit: formEvents, elementEvents, callType on events/commands, auto-detect extension mode (IDs 1000000+) - form-info: [EXTENSION] marker, callType on events/commands, BaseForm footer - form-validate: callType value checks, extension ID range warnings, BaseForm presence, callType-without-BaseForm detection - cfe-diff: form-level analysis in Mode A — borrowed/own forms, callType interceptors on events and commands - cfe-patch-method: warn if Form.xml missing for .Form. paths Co-Authored-By: Claude Opus 4.6 --- .claude/skills/cfe-borrow/SKILL.md | 14 + .../skills/cfe-borrow/scripts/cfe-borrow.ps1 | 322 ++++++++++++++++-- .claude/skills/cfe-diff/SKILL.md | 9 + .claude/skills/cfe-diff/scripts/cfe-diff.ps1 | 81 ++++- .../scripts/cfe-patch-method.ps1 | 15 + .claude/skills/form-edit/SKILL.md | 46 ++- .../skills/form-edit/scripts/form-edit.ps1 | 190 ++++++++++- .claude/skills/form-info/SKILL.md | 27 +- .../skills/form-info/scripts/form-info.ps1 | 46 ++- .claude/skills/form-validate/SKILL.md | 14 + .../form-validate/scripts/form-validate.ps1 | 146 ++++++++ 11 files changed, 866 insertions(+), 44 deletions(-) diff --git a/.claude/skills/cfe-borrow/SKILL.md b/.claude/skills/cfe-borrow/SKILL.md index eb6bd990..c39ab7be 100644 --- a/.claude/skills/cfe-borrow/SKILL.md +++ b/.claude/skills/cfe-borrow/SKILL.md @@ -30,9 +30,20 @@ allowed-tools: - `CommonModule.РаботаСФайлами` — общий модуль - `Document.РеализацияТоваров` — документ - `Enum.ВидыОплат` — перечисление +- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы) - `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов Поддерживаются все 44 типа объектов конфигурации. +### Заимствование форм + +Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически. + +Создаётся: +1. **Метаданные формы** — `Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed` +2. **Form.xml** — `Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `` (начальное состояние) +3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl` +4. **Регистрация** — `
` в ChildObjects родительского объекта + ## Команда ```powershell @@ -45,6 +56,9 @@ powershell.exe -NoProfile -File .claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 # Заимствовать один объект ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" +# Заимствовать форму (автоматически заимствует родительский объект) +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента" + # Несколько объектов за раз ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат" ``` diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 index 891b9df1..1a6a7314 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -384,6 +384,239 @@ function Read-SourceObject { } } +# --- 10b. Helper: read source form UUID --- +function Read-SourceFormUuid { + param([string]$typeName, [string]$objName, [string]$formName) + + $dirName = $childTypeDirMap[$typeName] + $srcFile = Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") "${formName}.xml" + if (-not (Test-Path $srcFile)) { + Write-Error "Source form not found: $srcFile" + exit 1 + } + + $srcDoc = New-Object System.Xml.XmlDocument + $srcDoc.PreserveWhitespace = $false + $srcDoc.Load($srcFile) + + $srcEl = $null + foreach ($c in $srcDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $srcEl = $c; break } + } + if (-not $srcEl) { + Write-Error "No metadata element found in source form: $srcFile" + exit 1 + } + + $srcUuid = $srcEl.GetAttribute("uuid") + if (-not $srcUuid) { + Write-Error "No uuid attribute on source form element: $srcFile" + exit 1 + } + + return $srcUuid +} + +# --- 10c. Helper: borrow a form --- +function Borrow-Form { + param([string]$typeName, [string]$objName, [string]$formName) + + $dirName = $childTypeDirMap[$typeName] + $enc = New-Object System.Text.UTF8Encoding($true) + + # 1. Read source form UUID + $formUuid = Read-SourceFormUuid $typeName $objName $formName + Info " Source form UUID: $formUuid" + + # 2. Read source Form.xml content + $srcFormXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml" + if (-not (Test-Path $srcFormXmlPath)) { + Write-Error "Source Form.xml not found: $srcFormXmlPath" + exit 1 + } + $srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc) + + # 3. Generate form metadata XML (ФормаЭлемента.xml) + $newFormUuid = [guid]::NewGuid().ToString() + $formMetaSb = New-Object System.Text.StringBuilder + $formMetaSb.AppendLine("") | Out-Null + $formMetaSb.AppendLine("") | Out-Null + $formMetaSb.AppendLine("`t") | Out-Null + $formMetaSb.AppendLine("`t`t") | Out-Null + $formMetaSb.AppendLine("`t`t") | Out-Null + $formMetaSb.AppendLine("`t`t`tAdopted") | Out-Null + $formMetaSb.AppendLine("`t`t`t${formName}") | Out-Null + $formMetaSb.AppendLine("`t`t`t") | Out-Null + $formMetaSb.AppendLine("`t`t`t${formUuid}") | Out-Null + $formMetaSb.AppendLine("`t`t`tManaged") | Out-Null + $formMetaSb.AppendLine("`t`t") | Out-Null + $formMetaSb.AppendLine("`t") | Out-Null + $formMetaSb.Append("
") | Out-Null + + # 4. Create directories + $formMetaDir = Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms" + if (-not (Test-Path $formMetaDir)) { + New-Item -ItemType Directory -Path $formMetaDir -Force | Out-Null + } + + # Write form metadata + $formMetaFile = Join-Path $formMetaDir "${formName}.xml" + [System.IO.File]::WriteAllText($formMetaFile, $formMetaSb.ToString(), $enc) + Info " Created: $formMetaFile" + + # 5. Generate Form.xml with BaseForm + # Extract inner content from source (everything between
and
) + $innerContent = "" + $formVersion = "2.17" + if ($srcFormContent -match '(?s)]*version="([^"]*)"[^>]*>(.*)') { + $formVersion = $Matches[1] + $innerContent = $Matches[2] + } elseif ($srcFormContent -match '(?s)]*>(.*)') { + $innerContent = $Matches[1] + } + + # Build the extension Form.xml: resultant form + + $formXmlSb = New-Object System.Text.StringBuilder + # Copy the original XML declaration and
opening tag + if ($srcFormContent -match '(?s)^(.*?]*>)') { + $formXmlSb.Append($Matches[1]) | Out-Null + } + # Resultant form content (same as source initially) + $formXmlSb.Append($innerContent) | Out-Null + # BaseForm section + $formXmlSb.AppendLine("`t") | Out-Null + # Inner content for BaseForm (trim leading newline) + $baseInner = $innerContent.TrimStart("`r", "`n") + $formXmlSb.Append("`t") | Out-Null + $formXmlSb.Append($baseInner) | Out-Null + # Close BaseForm — ensure it's on its own line + $lastChar = $formXmlSb.ToString()[-1] + if ($lastChar -ne "`n") { $formXmlSb.AppendLine() | Out-Null } + $formXmlSb.AppendLine("`t") | Out-Null + $formXmlSb.Append("") | Out-Null + + # Write Form.xml + $formXmlDir = Join-Path (Join-Path $formMetaDir $formName) "Ext" + if (-not (Test-Path $formXmlDir)) { + New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null + } + $formXmlFile = Join-Path $formXmlDir "Form.xml" + [System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc) + Info " Created: $formXmlFile" + + # 6. Create empty Module.bsl + $moduleDir = Join-Path $formXmlDir "Form" + if (-not (Test-Path $moduleDir)) { + New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null + } + $moduleBslFile = Join-Path $moduleDir "Module.bsl" + [System.IO.File]::WriteAllText($moduleBslFile, "", $enc) + Info " Created: $moduleBslFile" + + # 7. Register form in parent object ChildObjects + Register-FormInObject $typeName $objName $formName + + return @($formMetaFile, $formXmlFile, $moduleBslFile) +} + +# --- 10d. Helper: register form in parent object's ChildObjects --- +function Register-FormInObject { + param([string]$typeName, [string]$objName, [string]$formName) + + $dirName = $childTypeDirMap[$typeName] + $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" + + if (-not (Test-Path $objFile)) { + Warn "Parent object file not found: $objFile — form not registered in ChildObjects" + return + } + + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $true + $objDoc.Load($objFile) + + $objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + # Find the type element + $objEl = $null + foreach ($c in $objDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { + Warn "No type element in $objFile — form not registered" + return + } + + # Find or create ChildObjects + $childObjs = $objEl.SelectSingleNode("md:ChildObjects", $objNs) + if (-not $childObjs) { + # Create ChildObjects element + $childObjs = $objDoc.CreateElement("ChildObjects", "http://v8.1c.ru/8.3/MDClasses") + $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t`t")) | Out-Null + $objEl.AppendChild($childObjs) | Out-Null + $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t")) | Out-Null + } + + # Check dedup + foreach ($c in $childObjs.ChildNodes) { + if ($c.NodeType -eq 'Element' -and $c.LocalName -eq "Form" -and $c.InnerText -eq $formName) { + Warn "Form '$formName' already in ChildObjects of ${typeName}.${objName}" + return + } + } + + # Expand self-closing if needed + if (-not $childObjs.HasChildNodes -or $childObjs.IsEmpty) { + $closeWs = $objDoc.CreateWhitespace("`r`n`t`t") + $childObjs.AppendChild($closeWs) | Out-Null + } + + # Add
formName
+ $formEl = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") + $formEl.InnerText = $formName + + $trailing = $childObjs.LastChild + $ws = $objDoc.CreateWhitespace("`r`n`t`t`t") + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $childObjs.InsertBefore($ws, $trailing) | Out-Null + $childObjs.InsertBefore($formEl, $trailing) | Out-Null + } else { + $childObjs.AppendChild($ws) | Out-Null + $childObjs.AppendChild($formEl) | Out-Null + } + + # Save object XML + $settings2 = New-Object System.Xml.XmlWriterSettings + $settings2.Encoding = New-Object System.Text.UTF8Encoding($true) + $settings2.Indent = $false + $settings2.NewLineHandling = [System.Xml.NewLineHandling]::None + + $memStream2 = New-Object System.IO.MemoryStream + $writer2 = [System.Xml.XmlWriter]::Create($memStream2, $settings2) + $objDoc.Save($writer2) + $writer2.Flush(); $writer2.Close() + + $bytes2 = $memStream2.ToArray() + $memStream2.Close() + $text2 = [System.Text.Encoding]::UTF8.GetString($bytes2) + if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) } + $text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"') + + $utf8Bom2 = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2) + Info " Registered form in: $objFile" +} + +# --- 10e. Helper: check if object is already borrowed in extension --- +function Test-ObjectBorrowed { + param([string]$typeName, [string]$objName) + + $dirName = $childTypeDirMap[$typeName] + $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" + return (Test-Path $objFile) +} + # --- 11. Helper: generate InternalInfo XML --- function Build-InternalInfoXml { param([string]$typeName, [string]$objName, [string]$indent) @@ -534,11 +767,11 @@ $borrowedCount = 0 foreach ($item in $items) { $dotIdx = $item.IndexOf(".") if ($dotIdx -lt 1) { - Write-Error "Invalid format '${item}', expected 'Type.Name'" + Write-Error "Invalid format '${item}', expected 'Type.Name' or 'Type.Name.Form.FormName'" exit 1 } $typeName = $item.Substring(0, $dotIdx) - $objName = $item.Substring($dotIdx + 1) + $remainder = $item.Substring($dotIdx + 1) # Resolve Russian synonym to English type name if ($synonymMap.ContainsKey($typeName)) { $typeName = $synonymMap[$typeName] } @@ -548,34 +781,71 @@ foreach ($item in $items) { exit 1 } - $dirName = $childTypeDirMap[$typeName] - - Info "Borrowing ${typeName}.${objName}..." - - # Read source object - $src = Read-SourceObject $typeName $objName - Info " Source UUID: $($src.Uuid)" - - # Build borrowed object XML - $borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties - - # Create directory in extension if needed - $targetDir = Join-Path $extDir $dirName - if (-not (Test-Path $targetDir)) { - New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + # Check for .Form. pattern: Type.ObjName.Form.FormName + $formName = $null + $formIdx = $remainder.IndexOf(".Form.") + if ($formIdx -gt 0) { + $objName = $remainder.Substring(0, $formIdx) + $formName = $remainder.Substring($formIdx + 6) # skip ".Form." + } else { + $objName = $remainder } - # Write borrowed object XML with UTF-8 BOM - $targetFile = Join-Path $targetDir "${objName}.xml" - $enc = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc) - Info " Created: $targetFile" + $dirName = $childTypeDirMap[$typeName] - # Add to ChildObjects - Add-ToChildObjects $typeName $objName + if ($formName) { + # --- Form borrowing --- + Info "Borrowing form ${typeName}.${objName}.Form.${formName}..." - $borrowedFiles += $targetFile - $borrowedCount++ + # Auto-borrow parent object if not yet borrowed + if (-not (Test-ObjectBorrowed $typeName $objName)) { + Info " Parent object ${typeName}.${objName} not yet borrowed — borrowing first..." + + $src = Read-SourceObject $typeName $objName + Info " Source UUID: $($src.Uuid)" + $borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties + + $targetDir = Join-Path $extDir $dirName + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${objName}.xml" + $enc = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc) + Info " Created: $targetFile" + + Add-ToChildObjects $typeName $objName + $borrowedFiles += $targetFile + } + + # Borrow the form + $formFiles = Borrow-Form $typeName $objName $formName + $borrowedFiles += $formFiles + $borrowedCount++ + } else { + # --- Object borrowing (existing logic) --- + Info "Borrowing ${typeName}.${objName}..." + + $src = Read-SourceObject $typeName $objName + Info " Source UUID: $($src.Uuid)" + + $borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties + + $targetDir = Join-Path $extDir $dirName + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + + $targetFile = Join-Path $targetDir "${objName}.xml" + $enc = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc) + Info " Created: $targetFile" + + Add-ToChildObjects $typeName $objName + + $borrowedFiles += $targetFile + $borrowedCount++ + } } # --- 15. Save modified Configuration.xml --- diff --git a/.claude/skills/cfe-diff/SKILL.md b/.claude/skills/cfe-diff/SKILL.md index 3dd71dff..d56a4a2d 100644 --- a/.claude/skills/cfe-diff/SKILL.md +++ b/.claude/skills/cfe-diff/SKILL.md @@ -38,9 +38,18 @@ powershell.exe -NoProfile -File .claude/skills/cfe-diff/scripts/cfe-diff.ps1 -Ex &ИзменениеИКонтроль("РеквизитыРедактируемыеВГрупповойОбработке") — 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-события формы и элементов +- callType на командах + ## Mode B — проверка переноса Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации. diff --git a/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 b/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 index 4177d184..0f6dbdc4 100644 --- a/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 +++ b/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 @@ -210,6 +210,63 @@ function Get-InsertionBlocks { return $blocks } +# --- Helper: analyze form for callType events and commands --- +function Get-FormInterceptors { + param([string]$formXmlPath) + + if (-not (Test-Path $formXmlPath)) { return $null } + + $formDoc = New-Object System.Xml.XmlDocument + $formDoc.PreserveWhitespace = $false + try { $formDoc.Load($formXmlPath) } catch { return $null } + + $fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable) + $fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") + + $fRoot = $formDoc.DocumentElement + $baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs) + $isBorrowed = ($baseForm -ne $null) + + $interceptors = @() + + # Form-level events with callType + $eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs) + if ($eventsNode) { + foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) { + $ct = $evt.GetAttribute("callType") + if ($ct) { + $interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)" + } + } + } + + # Element-level events with callType (scan all elements recursively) + $childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs) + if ($childItems) { + foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) { + $elName = $evtNode.GetAttribute("name") + foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) { + $ct = $evt.GetAttribute("callType") + $interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)" + } + } + } + + # Commands with callType on Action + foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) { + $cmdName = $cmd.GetAttribute("name") + foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) { + $ct = $action.GetAttribute("callType") + $interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)" + } + } + + return @{ + IsBorrowed = $isBorrowed + Interceptors = $interceptors + } +} + # ============================================================ # MODE A: Extension overview # ============================================================ @@ -255,6 +312,7 @@ if ($Mode -eq "A") { $ownForms = 0 $ownTS = 0 $borrowedItems = 0 + $formNames = @() foreach ($c in $childObj.ChildNodes) { if ($c.NodeType -ne 'Element') { continue } $cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs) @@ -268,7 +326,7 @@ if ($Mode -eq "A") { switch ($c.LocalName) { "Attribute" { $ownAttrs++ } "TabularSection" { $ownTS++ } - "Form" { $ownForms++ } + "Form" { $formNames += $c.InnerText; $ownForms++ } } } $parts = @() @@ -279,6 +337,27 @@ if ($Mode -eq "A") { if ($parts.Count -gt 0) { Write-Host " ChildObjects: $($parts -join ', ')" } + + # Analyze forms + $borrowedFormCount = 0 + $ownFormCount = 0 + foreach ($fn in $formNames) { + $formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml" + $fi = Get-FormInterceptors $formXmlPath + if (-not $fi) { + Write-Host " Form.$fn (?)" + continue + } + $formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ } + if ($fi.Interceptors.Count -gt 0) { + Write-Host " Form.$fn ($formTag):" + foreach ($ic in $fi.Interceptors) { + Write-Host " $ic" + } + } else { + Write-Host " Form.$fn ($formTag)" + } + } } } } else { diff --git a/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 index 7c3bf2d0..d101d326 100644 --- a/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 +++ b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 @@ -157,6 +157,21 @@ $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 -Object `"$objType.$objName.Form.$formName`"" + Write-Host "" + } +} + # --- Check if file exists and append --- $bslDir = Split-Path $bslFile -Parent if (-not (Test-Path $bslDir)) { diff --git a/.claude/skills/form-edit/SKILL.md b/.claude/skills/form-edit/SKILL.md index 03c663c7..b533d3db 100644 --- a/.claude/skills/form-edit/SKILL.md +++ b/.claude/skills/form-edit/SKILL.md @@ -50,6 +50,32 @@ powershell.exe -NoProfile -File .claude/skills/form-edit/scripts/form-edit.ps1 - } ``` +### Расширения (extension-формы) + +Для заимствованных форм (с ``) автоматически активируется extension-режим: ID начинаются с 1000000+. Доступны дополнительные секции: + +```json +{ + "formEvents": [ + { "name": "OnCreateAtServer", "handler": "Расш1_ПриСозданииПосле", "callType": "After" }, + { "name": "OnOpen", "handler": "Расш1_ПриОткрытии", "callType": "Before" } + ], + "elementEvents": [ + { "element": "Банк", "name": "OnChange", "handler": "Расш1_БанкПриИзменении", "callType": "Before" } + ], + "commands": [ + { "name": "Подбор", "action": "Расш1_ПодборПосле", "callType": "After" }, + { "name": "Запрос", "actions": [ + { "callType": "Before", "handler": "Расш1_ЗапросПеред" }, + { "callType": "After", "handler": "Расш1_ЗапросПосле" } + ]} + ], + "elements": [ + { "input": "Поле", "path": "Объект.Поле", "on": [{ "event": "OnChange", "callType": "After" }] } + ] +} +``` + ### Позиционирование элементов | Ключ | По умолчанию | Описание | @@ -96,19 +122,35 @@ powershell.exe -NoProfile -File .claude/skills/form-edit/scripts/form-edit.ps1 - `string`, `string(100)`, `decimal(15,2)`, `boolean`, `date`, `dateTime`, `CatalogRef.XXX`, `DocumentObject.XXX`, `ValueTable`, `DynamicList`, `Type1 | Type2` (составной). +### Секции расширений + +| Секция | Назначение | +|--------|-----------| +| `formEvents` | События уровня формы с `callType` (Before/After/Override) | +| `elementEvents` | События на существующих элементах заимствованной формы | +| `callType` на `commands` | callType на Action команды | +| `callType` на `on` | callType на событиях новых элементов (объектный формат) | + +Все 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=12) + + СуммаИтого: decimal(15,2) (id=1000000) --- -Total: 1 element(s) (+2 companions), 1 attribute(s) +Total: 1 form event(s), 1 element(s) (+2 companions), 1 attribute(s) Run /form-validate to verify. ``` diff --git a/.claude/skills/form-edit/scripts/form-edit.ps1 b/.claude/skills/form-edit/scripts/form-edit.ps1 index 65add737..d48de803 100644 --- a/.claude/skills/form-edit/scripts/form-edit.ps1 +++ b/.claude/skills/form-edit/scripts/form-edit.ps1 @@ -110,6 +110,16 @@ $script:nextElemId++ $script:nextAttrId++ $script:nextCmdId++ +# --- 4b. Auto-detect extension mode (BaseForm present) --- +$script:isExtension = $false +$baseForm = $root.SelectSingleNode("f:BaseForm", $nsMgr) +if ($baseForm) { + $script:isExtension = $true + if ($script:nextAttrId -lt 1000000) { $script:nextAttrId = 1000000 } + if ($script:nextCmdId -lt 1000000) { $script:nextCmdId = 1000000 } + if ($script:nextElemId -lt 1000000) { $script:nextElemId = 1000000 } +} + function New-ElemId { $id = $script:nextElemId; $script:nextElemId++; return $id } function New-AttrId { $id = $script:nextAttrId; $script:nextAttrId++; return $id } function New-CmdId { $id = $script:nextCmdId; $script:nextCmdId++; return $id } @@ -260,18 +270,29 @@ function Emit-Events { if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) { $allowed = $script:knownEvents[$typeKey] foreach ($evt in $el.on) { - if ($allowed.Count -gt 0 -and $allowed -notcontains "$evt") { - Write-Host "[WARN] Unknown event '$evt' for $typeKey '$elementName'. Known: $($allowed -join ', ')" + $evtStr = if ($evt -is [string]) { "$evt" } else { "$($evt.event)" } + if ($allowed.Count -gt 0 -and $allowed -notcontains $evtStr) { + Write-Host "[WARN] Unknown event '$evtStr' for $typeKey '$elementName'. Known: $($allowed -join ', ')" } } } X "$indent" foreach ($evt in $el.on) { - $evtName = "$evt" - $handler = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } - else { Get-HandlerName -elementName $elementName -eventName $evtName } - X "$indent`t$handler" + # Support both string ("OnChange") and object ({ "event": "OnChange", "callType": "After" }) + if ($evt -is [string] -or -not $evt.event) { + $evtName = "$evt" + $handler = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } + else { Get-HandlerName -elementName $elementName -eventName $evtName } + X "$indent`t$handler" + } else { + $evtName = "$($evt.event)" + $handler = if ($evt.handler) { "$($evt.handler)" } + elseif ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } + else { Get-HandlerName -elementName $elementName -eventName $evtName } + $callTypeAttr = if ($evt.callType) { " callType=`"$($evt.callType)`"" } else { "" } + X "$indent`t$handler" + } } X "$indent" } @@ -964,7 +985,20 @@ if ($def.commands -and $def.commands.Count -gt 0) { $inner = "$cmdChildIndent`t" if ($cmd.title) { Emit-MLText -tag "Title" -text "$($cmd.title)" -indent $inner } - if ($cmd.action) { X "$inner$($cmd.action)" } + + # Support single action with optional callType, or multiple actions + if ($cmd.actions) { + # Multiple actions: [{ "callType": "Before", "handler": "..." }, ...] + foreach ($act in $cmd.actions) { + $actHandler = "$($act.handler)" + $callTypeAttr = if ($act.callType) { " callType=`"$($act.callType)`"" } else { "" } + X "$inner$actHandler
" + } + } elseif ($cmd.action) { + $callTypeAttr = if ($cmd.callType) { " callType=`"$($cmd.callType)`"" } else { "" } + X "$inner$($cmd.action)
" + } + if ($cmd.shortcut) { X "$inner$($cmd.shortcut)" } if ($cmd.picture) { X "$inner" @@ -975,7 +1009,7 @@ if ($def.commands -and $def.commands.Count -gt 0) { if ($cmd.representation) { X "$inner$($cmd.representation)" } X "$cmdChildIndent" - $actionStr = if ($cmd.action) { " -> $($cmd.action)" } else { "" } + $actionStr = if ($cmd.action) { " -> $($cmd.action)" } elseif ($cmd.actions) { " -> $($cmd.actions.Count) action(s)" } else { "" } $addedCmds += " + ${cmdName}${actionStr} (id=$cmdId)" } X "" @@ -988,6 +1022,127 @@ if ($def.commands -and $def.commands.Count -gt 0) { } } +# === 12b. Add form-level events === + +$addedFormEvents = @() + +if ($def.formEvents -and $def.formEvents.Count -gt 0) { + $eventsSection = $root.SelectSingleNode("f:Events", $nsMgr) + if (-not $eventsSection) { + # Create Events section — insert after AutoCommandBar or at the beginning + $eventsSection = $xmlDoc.CreateElement("Events", $formNs) + $insertAfter = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) + if ($insertAfter) { + $refNode = $insertAfter + $ws = $xmlDoc.CreateWhitespace("`r`n`t") + # Insert before the AutoCommandBar (Events come before AutoCommandBar in 1C) + $root.InsertBefore($ws, $refNode) | Out-Null + $root.InsertBefore($eventsSection, $refNode) | Out-Null + } else { + $firstChild = $root.FirstChild + if ($firstChild) { + $ws = $xmlDoc.CreateWhitespace("`r`n`t") + $root.InsertBefore($eventsSection, $firstChild) | Out-Null + $root.InsertBefore($ws, $eventsSection) | Out-Null + } else { + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null + $root.AppendChild($eventsSection) | Out-Null + } + } + } + + $evtChildIndent = Get-ChildIndent $eventsSection + if (-not $evtChildIndent -or $evtChildIndent -eq "") { $evtChildIndent = "`t`t" } + + # Generate event fragments + $script:xml = New-Object System.Text.StringBuilder 512 + X "<_F $allNsDecl>" + foreach ($fe in $def.formEvents) { + $feName = "$($fe.name)" + $feHandler = "$($fe.handler)" + $callTypeAttr = if ($fe.callType) { " callType=`"$($fe.callType)`"" } else { "" } + X "$evtChildIndent$feHandler" + $ctStr = if ($fe.callType) { "[$($fe.callType)]" } else { "" } + $addedFormEvents += " + $feName${ctStr} -> $feHandler" + } + X "" + + $fragDoc = Parse-Fragment $script:xml.ToString() + $importedEvents = Import-ElementNodes $fragDoc + + foreach ($node in $importedEvents) { + Insert-IntoContainer -container $eventsSection -newNode $node -afterName $null -childIndent $evtChildIndent + } +} + +# === 12c. Add element-level events === + +$addedElemEvents = @() + +if ($def.elementEvents -and $def.elementEvents.Count -gt 0) { + if (-not $rootCI) { + $rootCI = $root.SelectSingleNode("f:ChildItems", $nsMgr) + } + + foreach ($ee in $def.elementEvents) { + $targetName = "$($ee.element)" + $targetEl = Find-Element $rootCI $targetName + if (-not $targetEl) { + Write-Host "[WARN] Element '$targetName' not found — skipping elementEvent" + continue + } + + # Find or create Events element within the target + $targetEvents = $targetEl.SelectSingleNode("f:Events", $nsMgr) + if (-not $targetEvents) { + $targetEvents = $xmlDoc.CreateElement("Events", $formNs) + # Insert Events before closing tag (after last property, before ChildItems if any) + $ciNode = $targetEl.SelectSingleNode("f:ChildItems", $nsMgr) + if ($ciNode) { + $ws = $xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl)) + $targetEl.InsertBefore($ws, $ciNode) | Out-Null + $targetEl.InsertBefore($targetEvents, $ciNode) | Out-Null + } else { + $trailing = $targetEl.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $ws = $xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl)) + $targetEl.InsertBefore($ws, $trailing) | Out-Null + $targetEl.InsertBefore($targetEvents, $trailing) | Out-Null + } else { + $targetEl.AppendChild($xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl))) | Out-Null + $targetEl.AppendChild($targetEvents) | Out-Null + } + } + } + + $eeChildIndent = Get-ChildIndent $targetEvents + if (-not $eeChildIndent -or $eeChildIndent -eq "") { + $parentIndent = Get-ChildIndent $targetEl + $eeChildIndent = "$parentIndent`t" + } + + # Create Event element + $eeName = "$($ee.name)" + $eeHandler = "$($ee.handler)" + $callTypeAttr = if ($ee.callType) { " callType=`"$($ee.callType)`"" } else { "" } + + $script:xml = New-Object System.Text.StringBuilder 256 + X "<_F $allNsDecl>" + X "$eeChildIndent$eeHandler" + X "" + + $fragDoc = Parse-Fragment $script:xml.ToString() + $importedEE = Import-ElementNodes $fragDoc + + foreach ($node in $importedEE) { + Insert-IntoContainer -container $targetEvents -newNode $node -afterName $null -childIndent $eeChildIndent + } + + $ctStr = if ($ee.callType) { "[$($ee.callType)]" } else { "" } + $addedElemEvents += " + $targetName.$eeName${ctStr} -> $eeHandler" + } +} + # === 13. Save === $content = $xmlDoc.OuterXml @@ -999,6 +1154,23 @@ $enc = New-Object System.Text.UTF8Encoding($true) # === 14. Summary === +if ($script:isExtension) { + Write-Host "[EXTENSION] BaseForm detected — IDs start at 1000000+" + Write-Host "" +} + +if ($addedFormEvents.Count -gt 0) { + Write-Host "Added form events:" + foreach ($line in $addedFormEvents) { Write-Host $line } + Write-Host "" +} + +if ($addedElemEvents.Count -gt 0) { + Write-Host "Added element events:" + foreach ($line in $addedElemEvents) { Write-Host $line } + Write-Host "" +} + if ($addedElems.Count -gt 0) { $posStr = "" if ($def.into) { $posStr += "into $($def.into)" } @@ -1023,6 +1195,8 @@ if ($addedCmds.Count -gt 0) { Write-Host "---" $totalParts = @() +if ($addedFormEvents.Count -gt 0) { $totalParts += "$($addedFormEvents.Count) form event(s)" } +if ($addedElemEvents.Count -gt 0) { $totalParts += "$($addedElemEvents.Count) element event(s)" } if ($addedElems.Count -gt 0) { $compStr = if ($companionCount -gt 0) { " (+$companionCount companions)" } else { "" } $totalParts += "$($addedElems.Count) element(s)$compStr" diff --git a/.claude/skills/form-info/SKILL.md b/.claude/skills/form-info/SKILL.md index 69944999..760d5bd7 100644 --- a/.claude/skills/form-info/SKILL.md +++ b/.claude/skills/form-info/SKILL.md @@ -45,6 +45,11 @@ powershell.exe -NoProfile -File .claude/skills/form-info/scripts/form-info.ps1 - === Form: ФормаДокумента — "Реализация товаров и услуг" (Documents.РеализацияТоваровУслуг) === ``` +Для заимствованных форм расширения (с ``): +``` +=== Form: ФормаЭлемента [EXTENSION] (Catalogs.Валюты) === +``` + Имя формы, заголовок (Title) и контекст объекта определяются из пути к файлу и XML. ### Properties — свойства формы @@ -63,6 +68,13 @@ Events: OnOpen -> ПриОткрытии ``` +Для расширений с callType: +``` +Events: + OnCreateAtServer[After] -> Расш1_ПриСозданииПосле + OnOpen[Before] -> Расш1_ПриОткрытии +``` + ### Elements — дерево UI-элементов Компактное дерево с типами, привязками к данным, флагами и событиями: @@ -114,7 +126,7 @@ Elements: **Привязка к команде**: `-> ИмяКоманды [cmd]` — команда формы, `-> Close [std]` — стандартная команда -**События**: `{OnChange, StartChoice}` — имена обработчиков +**События**: `{OnChange, StartChoice}` — имена обработчиков; `{OnChange[Before]}` — с callType для расширений **Заголовок**: `[title:Текст]` — только если отличается от имени элемента @@ -151,8 +163,21 @@ Commands: Заполнить -> ЗаполнитьОбработка ``` +Для расширений с callType на Action: +``` +Commands: + Подбор -> Расш1_ПодборПеред[Before], Расш1_ПодборПосле[After] +``` + Формат: `Имя -> Обработчик [Сочетание]` +### BaseForm (расширения) + +Для заимствованных форм в конце выводится: +``` +BaseForm: present (version 2.17) +``` + ## Что пропускается Скрипт убирает 80%+ XML-объёма: diff --git a/.claude/skills/form-info/scripts/form-info.ps1 b/.claude/skills/form-info/scripts/form-info.ps1 index 0604f887..68f97be5 100644 --- a/.claude/skills/form-info/scripts/form-info.ps1 +++ b/.claude/skills/form-info/scripts/form-info.ps1 @@ -35,6 +35,10 @@ $ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings $root = $xmlDoc.DocumentElement +# --- Detect extension (BaseForm) --- +$baseFormNode = $root.SelectSingleNode("d:BaseForm", $ns) +$isExtension = ($baseFormNode -ne $null) + # --- Helper: extract multilang text --- function Get-MLText($node) { @@ -140,7 +144,10 @@ function Get-EventsStr($node) { if (-not $eventsNode) { return "" } $evts = @() foreach ($e in $eventsNode.SelectNodes("d:Event", $ns)) { - $evts += $e.GetAttribute("name") + $eName = $e.GetAttribute("name") + $ct = $e.GetAttribute("callType") + if ($ct) { $evts += "$eName[$ct]" } + else { $evts += $eName } } if ($evts.Count -eq 0) { return "" } return " {$($evts -join ', ')}" @@ -340,7 +347,8 @@ if ($titleNode) { $formTitle = Get-MLText $titleNode if (-not $formTitle) { $formTitle = $titleNode.InnerText } } -$header = "=== Form: $formName" +$extMarker = if ($isExtension) { " [EXTENSION]" } else { "" } +$header = "=== Form: $formName$extMarker" if ($formTitle) { $header += " — `"$formTitle`"" } if ($objectContext) { $header += " ($objectContext)" } $header += " ===" @@ -390,7 +398,9 @@ if ($formEvents -and $formEvents.HasChildNodes) { foreach ($e in $formEvents.SelectNodes("d:Event", $ns)) { $eName = $e.GetAttribute("name") $eHandler = $e.InnerText - $lines += " $eName -> $eHandler" + $ct = $e.GetAttribute("callType") + $ctStr = if ($ct) { "[$ct]" } else { "" } + $lines += " $eName${ctStr} -> $eHandler" } } @@ -491,12 +501,27 @@ if ($cmdsNode) { $cmdLines = @() foreach ($cmd in $cmdsNode.SelectNodes("d:Command", $ns)) { $cName = $cmd.GetAttribute("name") - $action = $cmd.SelectSingleNode("d:Action", $ns) $shortcut = $cmd.SelectSingleNode("d:Shortcut", $ns) - - $actionStr = if ($action) { " -> $($action.InnerText)" } else { "" } $scStr = if ($shortcut) { " [$($shortcut.InnerText)]" } else { "" } + # Collect all Action elements (may have multiple with callType) + $actions = $cmd.SelectNodes("d:Action", $ns) + if ($actions.Count -gt 1) { + $actParts = @() + foreach ($a in $actions) { + $ct = $a.GetAttribute("callType") + $ctStr = if ($ct) { "[$ct]" } else { "" } + $actParts += "$($a.InnerText)$ctStr" + } + $actionStr = " -> $($actParts -join ', ')" + } elseif ($actions.Count -eq 1) { + $ct = $actions[0].GetAttribute("callType") + $ctStr = if ($ct) { "[$ct]" } else { "" } + $actionStr = " -> $($actions[0].InnerText)$ctStr" + } else { + $actionStr = "" + } + $cmdLines += " $cName$actionStr$scStr" } if ($cmdLines.Count -gt 0) { @@ -506,6 +531,15 @@ if ($cmdsNode) { } } +# --- BaseForm footer --- + +if ($isExtension) { + $bfVersion = $baseFormNode.GetAttribute("version") + $bfStr = if ($bfVersion) { "present (version $bfVersion)" } else { "present" } + $lines += "" + $lines += "BaseForm: $bfStr" +} + # --- Truncation protection --- $totalLines = $lines.Count diff --git a/.claude/skills/form-validate/SKILL.md b/.claude/skills/form-validate/SKILL.md index fd301e7a..b781eba8 100644 --- a/.claude/skills/form-validate/SKILL.md +++ b/.claude/skills/form-validate/SKILL.md @@ -46,6 +46,10 @@ powershell.exe -NoProfile -File .claude/skills/form-validate/scripts/form-valida | 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 | ## Вывод @@ -71,8 +75,18 @@ All checks passed. Код возврата: 0 = все проверки пройдены, 1 = есть ошибки. +### Расширения + +При обнаружении `` автоматически активируются дополнительные проверки: +- Валидность значений `callType` (Before/After/Override) +- ID расширения >= 1000000 для добавленных атрибутов и команд +- Наличие version на `` + +Формы без `` проверяются только стандартными проверками. + ## Когда использовать - **После `/form-compile`**: проверить корректность сгенерированной формы +- **После `/form-edit`**: проверить добавленные элементы, особенно в extension-формах - **После ручного редактирования Form.xml**: убедиться что ID уникальны, companions на месте, ссылки валидны - **При отладке**: выявить ошибки в структуре формы до сборки EPF diff --git a/.claude/skills/form-validate/scripts/form-validate.ps1 b/.claude/skills/form-validate/scripts/form-validate.ps1 index 33df9299..c51b8dae 100644 --- a/.claude/skills/form-validate/scripts/form-validate.ps1 +++ b/.claude/skills/form-validate/scripts/form-validate.ps1 @@ -479,6 +479,152 @@ if (-not $stopped) { } } +# --- Check 11: Extension-specific validations --- + +$baseFormNode = $root.SelectSingleNode("f:BaseForm", $nsMgr) +$isExtension = ($baseFormNode -ne $null) + +if (-not $stopped -and $isExtension) { + # 11a. BaseForm version + $bfVersion = $baseFormNode.GetAttribute("version") + if ($bfVersion) { + Report-OK "BaseForm: version=$bfVersion" + } else { + Report-Warn "BaseForm: version attribute missing" + } + + # 11b. callType values validation (Before, After, Override) + $validCallTypes = @("Before", "After", "Override") + $ctErrors = 0 + $ctChecked = 0 + + # Check form-level events + $formEventsNode = $root.SelectSingleNode("f:Events", $nsMgr) + if ($formEventsNode) { + foreach ($evt in $formEventsNode.SelectNodes("f:Event", $nsMgr)) { + $ct = $evt.GetAttribute("callType") + if ($ct) { + $ctChecked++ + if ($validCallTypes -notcontains $ct) { + Report-Error "Form event '$($evt.GetAttribute('name'))': invalid callType='$ct' (expected: Before, After, Override)" + $ctErrors++ + } + } + } + } + + # Check element-level events + foreach ($el in $allElements) { + if ($stopped) { break } + $eventsNode = $el.Node.SelectSingleNode("f:Events", $nsMgr) + if (-not $eventsNode) { continue } + foreach ($evt in $eventsNode.SelectNodes("f:Event", $nsMgr)) { + $ct = $evt.GetAttribute("callType") + if ($ct) { + $ctChecked++ + if ($validCallTypes -notcontains $ct) { + Report-Error "[$($el.Tag)] '$($el.Name)' event '$($evt.GetAttribute('name'))': invalid callType='$ct'" + $ctErrors++ + } + } + } + } + + # Check command actions + foreach ($cmd in $cmdNodes) { + if ($stopped) { break } + $cmdName = $cmd.GetAttribute("name") + foreach ($action in $cmd.SelectNodes("f:Action", $nsMgr)) { + $ct = $action.GetAttribute("callType") + if ($ct) { + $ctChecked++ + if ($validCallTypes -notcontains $ct) { + Report-Error "Command '$cmdName' Action: invalid callType='$ct'" + $ctErrors++ + } + } + } + } + + if (-not $stopped -and $ctErrors -eq 0 -and $ctChecked -gt 0) { + Report-OK "callType values: $ctChecked checked" + } + + # 11c. Extension ID ranges — warn if extension-added attrs/commands have id < 1000000 + # Collect BaseForm attribute names to distinguish added ones + $baseAttrNames = @{} + $baseCmdNames = @{} + $bfNs = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) + $bfNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") + foreach ($bAttr in $baseFormNode.SelectNodes("f:Attributes/f:Attribute", $bfNs)) { + $baName = $bAttr.GetAttribute("name") + if ($baName) { $baseAttrNames[$baName] = $true } + } + foreach ($bCmd in $baseFormNode.SelectNodes("f:Commands/f:Command", $bfNs)) { + $bcName = $bCmd.GetAttribute("name") + if ($bcName) { $baseCmdNames[$bcName] = $true } + } + + $idWarnCount = 0 + foreach ($attr in $attrNodes) { + $aName = $attr.GetAttribute("name") + $aId = $attr.GetAttribute("id") + if ($aName -and -not $baseAttrNames.ContainsKey($aName) -and $aId) { + try { + $intId = [int]$aId + if ($intId -lt 1000000) { + Report-Warn "Attribute '$aName' (id=$aId): extension-added attribute has id < 1000000" + $idWarnCount++ + } + } catch {} + } + } + + foreach ($cmd in $cmdNodes) { + $cName = $cmd.GetAttribute("name") + $cId = $cmd.GetAttribute("id") + if ($cName -and -not $baseCmdNames.ContainsKey($cName) -and $cId) { + try { + $intId = [int]$cId + if ($intId -lt 1000000) { + Report-Warn "Command '$cName' (id=$cId): extension-added command has id < 1000000" + $idWarnCount++ + } + } catch {} + } + } + + if (-not $stopped -and $idWarnCount -eq 0) { + $extAttrCount = ($attrNodes | Where-Object { -not $baseAttrNames.ContainsKey($_.GetAttribute("name")) }).Count + $extCmdCount = ($cmdNodes | Where-Object { -not $baseCmdNames.ContainsKey($_.GetAttribute("name")) }).Count + if (($extAttrCount + $extCmdCount) -gt 0) { + Report-OK "Extension ID ranges: $extAttrCount attr(s), $extCmdCount cmd(s) — all >= 1000000" + } + } +} + +# Check callType without BaseForm (structural warning) +if (-not $stopped -and -not $isExtension) { + $callTypeWithoutBase = $false + $feNode = $root.SelectSingleNode("f:Events", $nsMgr) + if ($feNode) { + foreach ($evt in $feNode.SelectNodes("f:Event", $nsMgr)) { + if ($evt.GetAttribute("callType")) { $callTypeWithoutBase = $true; break } + } + } + if (-not $callTypeWithoutBase) { + foreach ($cmd in $cmdNodes) { + foreach ($action in $cmd.SelectNodes("f:Action", $nsMgr)) { + if ($action.GetAttribute("callType")) { $callTypeWithoutBase = $true; break } + } + if ($callTypeWithoutBase) { break } + } + } + if ($callTypeWithoutBase) { + Report-Warn "callType attributes found but no BaseForm — possible incorrect structure" + } +} + # --- Summary --- Write-Host "" From 3565e1c97ff4c73954ed0ba99a7eed4fd844858a Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 21 Feb 2026 16:39:27 +0300 Subject: [PATCH 3/5] =?UTF-8?q?fix(cfe):=206=20fixes=20from=20E2E=20test?= =?UTF-8?q?=20=E2=80=94=20Manager=20types,=20borrowed=20form=20structure,?= =?UTF-8?q?=20ConfigPath,=20guard=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. meta-compile + cfe-borrow: add Manager GeneratedType for Report/DataProcessor 2. cfe-borrow: rewrite Form.xml generation — extract only visual elements (AutoCommandBar + ChildItems), replace CommandName→0, strip Attributes/Events/Parameters 3. cfe-init: add -ConfigPath to auto-resolve Language UUID and CompatibilityMode 4. form-add: guard against overwriting existing Form.xml and Module.bsl 5. docs: update GeneratedType table for Report/DataProcessor 6. docs: rewrite section 5.4.2 with accurate borrowed form structure Co-Authored-By: Claude Opus 4.6 --- .../skills/cfe-borrow/scripts/cfe-borrow.ps1 | 119 +++++++++++++----- .claude/skills/cfe-init/SKILL.md | 14 ++- .claude/skills/cfe-init/scripts/cfe-init.ps1 | 55 +++++++- .claude/skills/form-add/scripts/form-add.ps1 | 12 +- .../meta-compile/scripts/meta-compile.ps1 | 6 +- docs/1c-config-objects-spec.md | 4 +- docs/1c-extension-spec.md | 49 +++++--- 7 files changed, 201 insertions(+), 58 deletions(-) diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 index 1a6a7314..98eb50c4 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -249,10 +249,12 @@ $script:generatedTypes = @{ @{ prefix = "DocumentJournalManager"; category = "Manager" } ) "Report" = @( - @{ prefix = "ReportObject"; category = "Object" } + @{ prefix = "ReportObject"; category = "Object" } + @{ prefix = "ReportManager"; category = "Manager" } ) "DataProcessor" = @( - @{ prefix = "DataProcessorObject"; category = "Object" } + @{ prefix = "DataProcessorObject"; category = "Object" } + @{ prefix = "DataProcessorManager"; category = "Manager" } ) } @@ -464,35 +466,96 @@ function Borrow-Form { [System.IO.File]::WriteAllText($formMetaFile, $formMetaSb.ToString(), $enc) Info " Created: $formMetaFile" - # 5. Generate Form.xml with BaseForm - # Extract inner content from source (everything between
and
) - $innerContent = "" - $formVersion = "2.17" - if ($srcFormContent -match '(?s)]*version="([^"]*)"[^>]*>(.*)') { - $formVersion = $Matches[1] - $innerContent = $Matches[2] - } elseif ($srcFormContent -match '(?s)]*>(.*)') { - $innerContent = $Matches[1] + # 5. Generate Form.xml with BaseForm (visual elements only) + # Parse source Form.xml as XmlDocument + $srcFormDoc = New-Object System.Xml.XmlDocument + $srcFormDoc.PreserveWhitespace = $true + $srcFormDoc.Load($srcFormXmlPath) + $srcFormEl = $srcFormDoc.DocumentElement + + $formVersion = $srcFormEl.GetAttribute("version") + if (-not $formVersion) { $formVersion = "2.17" } + + # Find direct children: AutoCommandBar, ChildItems (visual elements only) + $srcAutoCmd = $null + $srcChildItems = $null + foreach ($fc in $srcFormEl.ChildNodes) { + if ($fc.NodeType -ne 'Element') { continue } + if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) { $srcAutoCmd = $fc } + elseif ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) { $srcChildItems = $fc } } - # Build the extension Form.xml: resultant form + - $formXmlSb = New-Object System.Text.StringBuilder - # Copy the original XML declaration and
opening tag - if ($srcFormContent -match '(?s)^(.*?]*>)') { - $formXmlSb.Append($Matches[1]) | Out-Null + # Get OuterXml and strip redundant namespace redeclarations (they're on root ) + $nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"' + + $autoCmdXml = "" + if ($srcAutoCmd) { + $autoCmdXml = $srcAutoCmd.OuterXml + $autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '') + # Replace all CommandName values with 0 (base form buttons lose command refs) + $autoCmdXml = [regex]::Replace($autoCmdXml, '[^<]*', '0') + # Replace Autofill true → false + $autoCmdXml = $autoCmdXml -replace 'true', 'false' } - # Resultant form content (same as source initially) - $formXmlSb.Append($innerContent) | Out-Null - # BaseForm section - $formXmlSb.AppendLine("`t") | Out-Null - # Inner content for BaseForm (trim leading newline) - $baseInner = $innerContent.TrimStart("`r", "`n") - $formXmlSb.Append("`t") | Out-Null - $formXmlSb.Append($baseInner) | Out-Null - # Close BaseForm — ensure it's on its own line - $lastChar = $formXmlSb.ToString()[-1] - if ($lastChar -ne "`n") { $formXmlSb.AppendLine() | Out-Null } - $formXmlSb.AppendLine("`t") | Out-Null + + $childItemsXml = "" + if ($srcChildItems) { + $childItemsXml = $srcChildItems.OuterXml + $childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '') + # Replace all CommandName values with 0 in ChildItems too + $childItemsXml = [regex]::Replace($childItemsXml, '[^<]*', '0') + } else { + $childItemsXml = "" + } + + # Extract the opening tag from source text (preserves namespace declarations) + $xmlDecl = '' + $formTag = "" + if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] } + if ($srcFormContent -match '(]*>)') { $formTag = $Matches[1] } + + # Build output Form.xml + $formXmlSb = New-Object System.Text.StringBuilder + $formXmlSb.Append($xmlDecl) | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + $formXmlSb.Append($formTag) | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + + # Part 1: visual elements (add leading tab to first line of each block) + if ($autoCmdXml) { + $formXmlSb.Append("`t$autoCmdXml") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + } + $formXmlSb.Append("`t$childItemsXml") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + $formXmlSb.Append("`t") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + + # BaseForm: same visual elements, indented one more level + $formXmlSb.Append("`t") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + + if ($autoCmdXml) { + # Reindent for BaseForm: first line gets 2 tabs, other lines get +1 tab + $acLines = $autoCmdXml -split "`r`n" + for ($li = 0; $li -lt $acLines.Count; $li++) { + if ($li -eq 0) { $formXmlSb.Append("`t`t$($acLines[$li])") | Out-Null } + else { $formXmlSb.Append("`t$($acLines[$li])") | Out-Null } + $formXmlSb.Append("`r`n") | Out-Null + } + } + + $ciLines = $childItemsXml -split "`r`n" + for ($li = 0; $li -lt $ciLines.Count; $li++) { + if ($li -eq 0) { $formXmlSb.Append("`t`t$($ciLines[$li])") | Out-Null } + else { $formXmlSb.Append("`t$($ciLines[$li])") | Out-Null } + $formXmlSb.Append("`r`n") | Out-Null + } + + $formXmlSb.Append("`t`t") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + $formXmlSb.Append("`t") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null $formXmlSb.Append("") | Out-Null # Write Form.xml diff --git a/.claude/skills/cfe-init/SKILL.md b/.claude/skills/cfe-init/SKILL.md index 647035fb..c1c73602 100644 --- a/.claude/skills/cfe-init/SKILL.md +++ b/.claude/skills/cfe-init/SKILL.md @@ -1,7 +1,7 @@ --- name: cfe-init description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации -argument-hint: [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24] +argument-hint: [-ConfigPath ] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24] allowed-tools: - Bash - Read @@ -14,14 +14,14 @@ allowed-tools: ## Подготовка -Перед созданием расширения рекомендуется получить версию и режим совместимости базовой конфигурации: +Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации. + +Если `-ConfigPath` не задан, рекомендуется предварительно получить режим совместимости: ``` /cf-info -Mode brief ``` -Это даст `CompatibilityMode` (передать в `-CompatibilityMode`) и версию конфигурации (для `-Version`, например `<ВерсияКонфигурации>.1`). - ## Параметры | Параметр | Описание | По умолчанию | @@ -34,6 +34,7 @@ allowed-tools: | `Version` | Версия расширения | — | | `Vendor` | Поставщик | — | | `CompatibilityMode` | Режим совместимости | `Version8_3_24` | +| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — | | `NoRole` | Без основной роли | false | ## Команда @@ -56,7 +57,10 @@ powershell.exe -NoProfile -File .claude/skills/cfe-init/scripts/cfe-init.ps1 -Na ## Примеры ```powershell -# Расширение-исправление для ERP +# Расширение для ERP с авто-определением совместимости из базовой конфигурации +... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src + +# Расширение-исправление с явным режимом совместимости ... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src # Расширение-доработка с версией diff --git a/.claude/skills/cfe-init/scripts/cfe-init.ps1 b/.claude/skills/cfe-init/scripts/cfe-init.ps1 index b8f449ad..a74efba3 100644 --- a/.claude/skills/cfe-init/scripts/cfe-init.ps1 +++ b/.claude/skills/cfe-init/scripts/cfe-init.ps1 @@ -11,6 +11,7 @@ param( [string]$Version, [string]$Vendor, [string]$CompatibilityMode = "Version8_3_24", + [string]$ConfigPath, [switch]$NoRole ) @@ -34,6 +35,57 @@ if (Test-Path $cfgFile) { exit 1 } +# --- Resolve ConfigPath --- +$baseLangUuid = "00000000-0000-0000-0000-000000000000" +if ($ConfigPath) { + if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath + } + if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { $ConfigPath = $candidate } + else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 } + } + if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 } + $cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent + + # 3a. Read Language UUID from base config + $baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml" + if (Test-Path $baseLangFile) { + $baseLangDoc = New-Object System.Xml.XmlDocument + $baseLangDoc.PreserveWhitespace = $false + $baseLangDoc.Load($baseLangFile) + $langEl = $null + foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break } + } + if ($langEl) { + $baseLangUuid = $langEl.GetAttribute("uuid") + Write-Host "[INFO] Base config Language UUID: $baseLangUuid" + } else { + Write-Host "[WARN] No element in $baseLangFile" + } + } else { + Write-Host "[WARN] Base config language not found: $baseLangFile" + } + + # 3b. Read CompatibilityMode from base config + $baseCfgDoc = New-Object System.Xml.XmlDocument + $baseCfgDoc.PreserveWhitespace = $false + $baseCfgDoc.Load((Resolve-Path $ConfigPath).Path) + $baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable) + $baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs) + if ($compatNode -and $compatNode.InnerText) { + $CompatibilityMode = $compatNode.InnerText.Trim() + Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode" + } else { + Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode" + } +} else { + Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading." +} + # --- Generate UUIDs --- $uuidCfg = [guid]::NewGuid().ToString() $uuidLang = [guid]::NewGuid().ToString() @@ -149,7 +201,7 @@ $langXml = @" Adopted Русский - 00000000-0000-0000-0000-000000000000 + $baseLangUuid ru @@ -201,6 +253,7 @@ Write-Host "[OK] Создано расширение: $Name" Write-Host " Каталог: $OutputDir" Write-Host " Назначение: $Purpose" Write-Host " Префикс: $NamePrefix" +Write-Host " Совместимость: $CompatibilityMode" Write-Host " Configuration.xml: $cfgFile" Write-Host " Languages: $langFile" if (-not $NoRole) { diff --git a/.claude/skills/form-add/scripts/form-add.ps1 b/.claude/skills/form-add/scripts/form-add.ps1 index 871aad1b..dfb8dc1f 100644 --- a/.claude/skills/form-add/scripts/form-add.ps1 +++ b/.claude/skills/form-add/scripts/form-add.ps1 @@ -271,7 +271,11 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") { "@ } -[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom) +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 --- @@ -304,7 +308,11 @@ $moduleBsl = @" #КонецОбласти "@ -[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom) +if (Test-Path $modulePath) { + Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting" +} else { + [System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom) +} # --- Фаза 4: Регистрация в родительском объекте --- diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index 943716e1..09ebcf5e 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.ps1 +++ b/.claude/skills/meta-compile/scripts/meta-compile.ps1 @@ -478,10 +478,12 @@ $script:generatedTypes = @{ @{ prefix = "DocumentJournalManager"; category = "Manager" } ) "Report" = @( - @{ prefix = "ReportObject"; category = "Object" } + @{ prefix = "ReportObject"; category = "Object" } + @{ prefix = "ReportManager"; category = "Manager" } ) "DataProcessor" = @( - @{ prefix = "DataProcessorObject"; category = "Object" } + @{ prefix = "DataProcessorObject"; category = "Object" } + @{ prefix = "DataProcessorManager"; category = "Manager" } ) } diff --git a/docs/1c-config-objects-spec.md b/docs/1c-config-objects-spec.md index e23a44f5..c70b4ece 100644 --- a/docs/1c-config-objects-spec.md +++ b/docs/1c-config-objects-spec.md @@ -257,8 +257,8 @@ Ext/ # Расширение конфигураци | Task | Object, Ref, Selection, List, Manager | | ExchangePlan | Object, Ref, Selection, List, Manager | | DocumentJournal | Selection, List, Manager | -| Report | Object | -| DataProcessor | Object | +| Report | Object, Manager | +| DataProcessor | Object, Manager | Формат имени: `{ТипОбъектаEng}.{ИмяОбъекта}` (напр. `CatalogObject.Номенклатура`, `DocumentRef.АвансовыйОтчет`). diff --git a/docs/1c-extension-spec.md b/docs/1c-extension-spec.md index 43c00156..465ee2f4 100644 --- a/docs/1c-extension-spec.md +++ b/docs/1c-extension-spec.md @@ -356,7 +356,7 @@ Enums/ # Перечисления #### 5.4.2. Структура Form.xml заимствованной формы -Form.xml заимствованной формы — **двухчастный файл**: +Form.xml заимствованной формы — **двухчастный файл**: Part 1 (результирующая форма) и BaseForm (исходная форма). Обе части содержат **только визуальные элементы** — атрибуты, события, параметры и команды базовой конфигурации **НЕ включаются**. ```xml
@@ -364,38 +364,49 @@ Form.xml заимствованной формы — **двухчастный ф - - ... - + + + + + + + + Расш1_ПриСозданииПосле - - - - - ... - + Расш1_НоваяКомандаВместо - + - ... + + + + + + - + - - ... + + @@ -403,11 +414,13 @@ Form.xml заимствованной формы — **двухчастный ф **Ключевые правила:** -1. **Часть 1** (до ``) — **результирующая форма**, содержит ВСЕ элементы: и базовые, и добавленные расширением. Именно эта часть определяет итоговую форму при запуске. +1. **Часть 1** (до ``) — **результирующая форма**. Содержит визуальные элементы (AutoCommandBar + ChildItems) из базовой конфигурации плюс элементы расширения. Атрибуты базовой конфигурации (DynamicList, QueryText и др.) **не включаются** — только реквизиты расширения (id ≥ 1000000) или пустой ``. Events и Commands — только добавленные расширением (с `callType`). -2. **Часть 2** (``) — **полная копия оригинальной формы** из базовой конфигурации. Используется платформой для контроля совместимости — при обновлении конфигурации платформа сравнивает `` с текущей формой конфигурации и предупреждает о расхождениях. +2. **Часть 2** (``) — **визуальный снимок исходной формы**. Содержит только AutoCommandBar + ChildItems + пустой ``. НЕ содержит Events, Commands, Parameters. Все `` в кнопках заменены на `0`. Платформа использует BaseForm для контроля совместимости при обновлении конфигурации. -3. Элемент `` всегда идёт **последним** в `
` и имеет атрибут `version`. +3. **Правило `0`**: во всех кнопках базовой формы (как в Part 1, так и в BaseForm) значение `` заменяется на `0`. Ссылки на команды конфигурации не сохраняются. Только кнопки, добавленные расширением, сохраняют ссылку на команду (напр. `Form.Command.XXX`). + +4. Элемент `` всегда идёт **последним** в `` и имеет атрибут `version`. #### 5.4.3. Нумерация ID элементов From 6f32e18e37eeedea1609067c18712176a1660b7c Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 21 Feb 2026 17:22:05 +0300 Subject: [PATCH 4/5] improve(skills): auto-resolve ConfigPath, auto-register forms, handle LF line endings - cfe-init, cfe-borrow SKILL.md: auto-resolve ConfigPath from .v8-project.json configSrc - form-compile: auto-register in parent ChildObjects from OutputPath convention - form-compile SKILL.md: document new workflow (form-add no longer required first) - cfe-borrow: handle LF line endings in OuterXml split (-split "\r?\n") Co-Authored-By: Claude Opus 4.6 --- .claude/skills/cfe-borrow/SKILL.md | 8 +++ .../skills/cfe-borrow/scripts/cfe-borrow.ps1 | 4 +- .claude/skills/cfe-init/SKILL.md | 12 ++-- .claude/skills/form-compile/SKILL.md | 6 ++ .../form-compile/scripts/form-compile.ps1 | 65 +++++++++++++++++++ 5 files changed, 89 insertions(+), 6 deletions(-) diff --git a/.claude/skills/cfe-borrow/SKILL.md b/.claude/skills/cfe-borrow/SKILL.md index c39ab7be..0bf747f5 100644 --- a/.claude/skills/cfe-borrow/SKILL.md +++ b/.claude/skills/cfe-borrow/SKILL.md @@ -16,6 +16,14 @@ allowed-tools: Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`. +### Авто-определение ConfigPath + +Если пользователь не указал `-ConfigPath` — попробуй определить автоматически: +1. Прочитай `.v8-project.json` из корня проекта +2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`) +3. Если у базы есть поле `configSrc` — используй как `-ConfigPath` +4. Если `configSrc` нет — спроси у пользователя + ## Параметры | Параметр | Описание | diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 index 98eb50c4..0b526b35 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -537,7 +537,7 @@ function Borrow-Form { if ($autoCmdXml) { # Reindent for BaseForm: first line gets 2 tabs, other lines get +1 tab - $acLines = $autoCmdXml -split "`r`n" + $acLines = $autoCmdXml -split "`r?`n" for ($li = 0; $li -lt $acLines.Count; $li++) { if ($li -eq 0) { $formXmlSb.Append("`t`t$($acLines[$li])") | Out-Null } else { $formXmlSb.Append("`t$($acLines[$li])") | Out-Null } @@ -545,7 +545,7 @@ function Borrow-Form { } } - $ciLines = $childItemsXml -split "`r`n" + $ciLines = $childItemsXml -split "`r?`n" for ($li = 0; $li -lt $ciLines.Count; $li++) { if ($li -eq 0) { $formXmlSb.Append("`t`t$($ciLines[$li])") | Out-Null } else { $formXmlSb.Append("`t$($ciLines[$li])") | Out-Null } diff --git a/.claude/skills/cfe-init/SKILL.md b/.claude/skills/cfe-init/SKILL.md index c1c73602..5fac5bbf 100644 --- a/.claude/skills/cfe-init/SKILL.md +++ b/.claude/skills/cfe-init/SKILL.md @@ -16,11 +16,15 @@ allowed-tools: Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации. -Если `-ConfigPath` не задан, рекомендуется предварительно получить режим совместимости: +### Авто-определение ConfigPath -``` -/cf-info -Mode brief -``` +Если пользователь не указал `-ConfigPath` — попробуй определить автоматически: +1. Прочитай `.v8-project.json` из корня проекта +2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`) +3. Если у базы есть поле `configSrc` — используй как `-ConfigPath` +4. Если `configSrc` нет — спроси у пользователя + +Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию). ## Параметры diff --git a/.claude/skills/form-compile/SKILL.md b/.claude/skills/form-compile/SKILL.md index d2db3ba5..c4a991eb 100644 --- a/.claude/skills/form-compile/SKILL.md +++ b/.claude/skills/form-compile/SKILL.md @@ -395,6 +395,12 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile - **ID**: последовательная нумерация, AutoCommandBar = id="-1" - **Unknown keys**: выводится предупреждение о нераспознанных ключах +## Workflow + +1. **Компиляция**: `/form-compile` генерирует `Form.xml` и автоматически регистрирует `` в `ChildObjects` родительского объекта (если OutputPath следует конвенции `.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml`). +2. **Метаданные формы** (`ФормаСписка.xml`) и `Module.bsl` создаёт `/form-add`. Если `/form-add` ещё не вызывался — вызови после `/form-compile`. Он не перезаписывает существующий Form.xml. +3. **Проверка**: `/form-validate`, `/form-info`. + ## Верификация ``` diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 14a918ba..2f0652e6 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1137,6 +1137,71 @@ if (-not (Test-Path $outDir)) { $enc = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc) +# --- 13b. Auto-register form in parent object XML --- + +# Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml +$formXmlDir = [System.IO.Path]::GetDirectoryName($outPath) +$formNameDir = [System.IO.Path]::GetDirectoryName($formXmlDir) +$formsDir = [System.IO.Path]::GetDirectoryName($formNameDir) +$objectDir = [System.IO.Path]::GetDirectoryName($formsDir) +$typePluralDir = [System.IO.Path]::GetDirectoryName($objectDir) + +$formName = [System.IO.Path]::GetFileName($formNameDir) +$objectName = [System.IO.Path]::GetFileName($objectDir) +$formsLeaf = [System.IO.Path]::GetFileName($formsDir) + +if ($formsLeaf -eq 'Forms') { + $objectXmlPath = Join-Path $typePluralDir "$objectName.xml" + if (Test-Path $objectXmlPath) { + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $true + $objDoc.Load($objectXmlPath) + + $nsMgr = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $childObjects = $objDoc.SelectSingleNode("//md:ChildObjects", $nsMgr) + if ($childObjects) { + $existing = $childObjects.SelectSingleNode("md:Form[text()='$formName']", $nsMgr) + if (-not $existing) { + $formElem = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") + $formElem.InnerText = $formName + + $insertBefore = $childObjects.SelectSingleNode("md:Template", $nsMgr) + if (-not $insertBefore) { $insertBefore = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr) } + + if ($insertBefore) { + $childObjects.InsertBefore($formElem, $insertBefore) | Out-Null + $ws = $objDoc.CreateWhitespace("`n`t`t`t") + $childObjects.InsertBefore($ws, $insertBefore) | Out-Null + } else { + $lastChild = $childObjects.LastChild + if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) { + $childObjects.InsertBefore($objDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null + $childObjects.InsertBefore($formElem, $lastChild) | Out-Null + } else { + $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t`t")) | Out-Null + $childObjects.AppendChild($formElem) | Out-Null + $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t")) | Out-Null + } + } + + $regEnc = New-Object System.Text.UTF8Encoding($true) + $regSettings = New-Object System.Xml.XmlWriterSettings + $regSettings.Encoding = $regEnc + $regSettings.Indent = $false + $regStream = New-Object System.IO.FileStream($objectXmlPath, [System.IO.FileMode]::Create) + $regWriter = [System.Xml.XmlWriter]::Create($regStream, $regSettings) + $objDoc.Save($regWriter) + $regWriter.Close() + $regStream.Close() + + Write-Host " Registered: $formName in $objectName.xml" + } + } + } +} + # --- 14. Summary --- $elCount = $script:nextId - 1 From 5f7ee6fcae0a5343e965629fa64fe4280f9c7fda Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 21 Feb 2026 17:34:36 +0300 Subject: [PATCH 5/5] fix(form-validate): skip DataPath check for base elements in borrowed forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In extension forms with BaseForm, elements with id < 1000000 belong to the base configuration and their attributes are not present in the extension. Skip DataPath→Attribute validation for these elements to avoid false errors. Show "N base skipped" in output for transparency. Co-Authored-By: Claude Opus 4.6 --- .../form-validate/scripts/form-validate.ps1 | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.claude/skills/form-validate/scripts/form-validate.ps1 b/.claude/skills/form-validate/scripts/form-validate.ps1 index c51b8dae..fbdbdc32 100644 --- a/.claude/skills/form-validate/scripts/form-validate.ps1 +++ b/.claude/skills/form-validate/scripts/form-validate.ps1 @@ -76,6 +76,9 @@ if ($parentDir) { Write-Host "=== Validation: $formName ===" Write-Host "" +# Early BaseForm detection (used in Check 5 to skip base element DataPath validation) +$hasBaseForm = ($root.SelectSingleNode("f:BaseForm", $nsMgr) -ne $null) + # --- Check 1: Root element and version --- if ($root.LocalName -ne "Form") { @@ -297,6 +300,7 @@ if (-not $stopped) { if (-not $stopped) { $pathErrors = 0 $pathChecked = 0 + $pathBaseSkipped = 0 foreach ($el in $allElements) { if ($stopped) { break } @@ -309,6 +313,11 @@ if (-not $stopped) { continue } + # In borrowed forms, skip DataPath check for base elements (id < 1000000) + if ($hasBaseForm -and $el.Id) { + try { if ([int]$el.Id -lt 1000000) { $pathBaseSkipped++; continue } } catch {} + } + $dpNode = $node.SelectSingleNode("f:DataPath", $nsMgr) if (-not $dpNode) { continue } @@ -328,9 +337,15 @@ if (-not $stopped) { } } - if ($pathErrors -eq 0 -and $pathChecked -gt 0) { - Report-OK "DataPath references: $pathChecked paths checked" - } elseif ($pathChecked -eq 0) { + $pathMsg = "" + if ($pathChecked -gt 0) { $pathMsg = "$pathChecked paths checked" } + if ($pathBaseSkipped -gt 0) { + $skipNote = "$pathBaseSkipped base skipped" + $pathMsg = if ($pathMsg) { "$pathMsg, $skipNote" } else { $skipNote } + } + if ($pathErrors -eq 0 -and $pathMsg) { + Report-OK "DataPath references: $pathMsg" + } elseif ($pathErrors -eq 0) { Report-OK "DataPath references: none" } }