feat(cfe,form): add borrowed form support across 6 skills

- 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 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-02-21 14:59:16 +03:00
parent 9c1985b710
commit 28b967f591
11 changed files with 866 additions and 44 deletions
+14
View File
@@ -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` с копией исходной формы + `<BaseForm>` (начальное состояние)
3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl`
4. **Регистрация**`<Form>` в 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.ВидыОплат"
```
+296 -26
View File
@@ -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("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"2.17`">") | Out-Null
$formMetaSb.AppendLine("`t<Form uuid=`"${newFormUuid}`">") | Out-Null
$formMetaSb.AppendLine("`t`t<InternalInfo/>") | Out-Null
$formMetaSb.AppendLine("`t`t<Properties>") | Out-Null
$formMetaSb.AppendLine("`t`t`t<ObjectBelonging>Adopted</ObjectBelonging>") | Out-Null
$formMetaSb.AppendLine("`t`t`t<Name>${formName}</Name>") | Out-Null
$formMetaSb.AppendLine("`t`t`t<Comment/>") | Out-Null
$formMetaSb.AppendLine("`t`t`t<ExtendedConfigurationObject>${formUuid}</ExtendedConfigurationObject>") | Out-Null
$formMetaSb.AppendLine("`t`t`t<FormType>Managed</FormType>") | Out-Null
$formMetaSb.AppendLine("`t`t</Properties>") | Out-Null
$formMetaSb.AppendLine("`t</Form>") | Out-Null
$formMetaSb.Append("</MetaDataObject>") | 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 <Form ...> and </Form>)
$innerContent = ""
$formVersion = "2.17"
if ($srcFormContent -match '(?s)<Form[^>]*version="([^"]*)"[^>]*>(.*)</Form>') {
$formVersion = $Matches[1]
$innerContent = $Matches[2]
} elseif ($srcFormContent -match '(?s)<Form[^>]*>(.*)</Form>') {
$innerContent = $Matches[1]
}
# Build the extension Form.xml: resultant form + <BaseForm>
$formXmlSb = New-Object System.Text.StringBuilder
# Copy the original XML declaration and <Form> opening tag
if ($srcFormContent -match '(?s)^(.*?<Form[^>]*>)') {
$formXmlSb.Append($Matches[1]) | Out-Null
}
# Resultant form content (same as source initially)
$formXmlSb.Append($innerContent) | Out-Null
# BaseForm section
$formXmlSb.AppendLine("`t<BaseForm version=`"${formVersion}`">") | 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</BaseForm>") | Out-Null
$formXmlSb.Append("</Form>") | 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 <Form>formName</Form>
$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 ---
+9
View File
@@ -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 — проверка переноса
Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации.
+80 -1
View File
@@ -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 {
@@ -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 <ConfigPath> -Object `"$objType.$objName.Form.$formName`""
Write-Host ""
}
}
# --- Check if file exists and append ---
$bslDir = Split-Path $bslFile -Parent
if (-not (Test-Path $bslDir)) {
+44 -2
View File
@@ -50,6 +50,32 @@ powershell.exe -NoProfile -File .claude/skills/form-edit/scripts/form-edit.ps1 -
}
```
### Расширения (extension-формы)
Для заимствованных форм (с `<BaseForm>`) автоматически активируется 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.
```
+182 -8
View File
@@ -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<Events>"
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<Event name=`"$evtName`">$handler</Event>"
# 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<Event name=`"$evtName`">$handler</Event>"
} 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<Event name=`"$evtName`"$callTypeAttr>$handler</Event>"
}
}
X "$indent</Events>"
}
@@ -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<Action>$($cmd.action)</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<Action$callTypeAttr>$actHandler</Action>"
}
} elseif ($cmd.action) {
$callTypeAttr = if ($cmd.callType) { " callType=`"$($cmd.callType)`"" } else { "" }
X "$inner<Action$callTypeAttr>$($cmd.action)</Action>"
}
if ($cmd.shortcut) { X "$inner<Shortcut>$($cmd.shortcut)</Shortcut>" }
if ($cmd.picture) {
X "$inner<Picture>"
@@ -975,7 +1009,7 @@ if ($def.commands -and $def.commands.Count -gt 0) {
if ($cmd.representation) { X "$inner<Representation>$($cmd.representation)</Representation>" }
X "$cmdChildIndent</Command>"
$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 "</_F>"
@@ -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<Event name=`"$feName`"$callTypeAttr>$feHandler</Event>"
$ctStr = if ($fe.callType) { "[$($fe.callType)]" } else { "" }
$addedFormEvents += " + $feName${ctStr} -> $feHandler"
}
X "</_F>"
$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<Event name=`"$eeName`"$callTypeAttr>$eeHandler</Event>"
X "</_F>"
$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"
+26 -1
View File
@@ -45,6 +45,11 @@ powershell.exe -NoProfile -File .claude/skills/form-info/scripts/form-info.ps1 -
=== Form: ФормаДокумента — "Реализация товаров и услуг" (Documents.РеализацияТоваровУслуг) ===
```
Для заимствованных форм расширения (с `<BaseForm>`):
```
=== 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-объёма:
+40 -6
View File
@@ -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
+14
View File
@@ -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 = есть ошибки.
### Расширения
При обнаружении `<BaseForm>` автоматически активируются дополнительные проверки:
- Валидность значений `callType` (Before/After/Override)
- ID расширения >= 1000000 для добавленных атрибутов и команд
- Наличие version на `<BaseForm>`
Формы без `<BaseForm>` проверяются только стандартными проверками.
## Когда использовать
- **После `/form-compile`**: проверить корректность сгенерированной формы
- **После `/form-edit`**: проверить добавленные элементы, особенно в extension-формах
- **После ручного редактирования Form.xml**: убедиться что ID уникальны, companions на месте, ссылки валидны
- **При отладке**: выявить ошибки в структуре формы до сборки EPF
@@ -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 ""