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 ""