From a6bf6520cea131a5120a34ca894c613f9b1b1bde Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 14 Feb 2026 13:00:05 +0300 Subject: [PATCH] Add /meta-validate skill: structural validation of 1C metadata XML 11 check categories covering all 23 metadata types: root structure, InternalInfo, properties, enum values, StandardAttributes, ChildObjects, child elements (UUID/Name/Type), name uniqueness, TabularSections, cross-properties, HTTPService/WebService nested structure. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/meta-validate/SKILL.md | 112 ++ .../meta-validate/scripts/meta-validate.ps1 | 984 ++++++++++++++++++ 2 files changed, 1096 insertions(+) create mode 100644 .claude/skills/meta-validate/SKILL.md create mode 100644 .claude/skills/meta-validate/scripts/meta-validate.ps1 diff --git a/.claude/skills/meta-validate/SKILL.md b/.claude/skills/meta-validate/SKILL.md new file mode 100644 index 00000000..68460186 --- /dev/null +++ b/.claude/skills/meta-validate/SKILL.md @@ -0,0 +1,112 @@ +--- +name: meta-validate +description: Валидация структурной корректности объекта метаданных 1С (Справочник, Документ, Регистр, Перечисление и ещё 19 типов) +argument-hint: [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /meta-validate — валидация объекта метаданных 1С + +Проверяет XML объекта метаданных из выгрузки конфигурации на структурные ошибки: корневую структуру, InternalInfo, свойства, допустимые значения, StandardAttributes, ChildObjects, уникальность имён, табличные части, кросс-свойства, вложенные структуры HTTP/Web-сервисов. + +## Использование + +``` +/meta-validate +``` + +## Параметры + +| Параметр | Обязательный | По умолчанию | Описание | +|------------|:------------:|--------------|-------------------------------------------------| +| ObjectPath | да | — | Путь к XML-файлу или каталогу объекта | +| MaxErrors | нет | 30 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | + +`ObjectPath` авторезолв: если указана директория — ищет `/.xml`. + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\meta-validate\scripts\meta-validate.ps1 -ObjectPath "<путь>" +``` + +## Поддерживаемые типы (23) + +**Ссылочные:** Catalog, Document, Enum, ExchangePlan, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task +**Регистры:** InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister +**Отчёты/Обработки:** Report, DataProcessor +**Сервисные:** CommonModule, ScheduledJob, EventSubscription, HTTPService, WebService +**Прочие:** Constant, DocumentJournal, DefinedType + +## Выполняемые проверки + +| # | Проверка | Серьёзность | +|----|------------------------------------------|--------------| +| 1 | XML well-formedness + root structure | ERROR | +| 2 | InternalInfo / GeneratedType | ERROR / WARN | +| 3 | Properties — Name, Synonym | ERROR / WARN | +| 4 | Properties — enum-значения свойств | ERROR | +| 5 | StandardAttributes | ERROR / WARN | +| 6 | ChildObjects — допустимые элементы | ERROR | +| 7 | Attributes/Dimensions/Resources — UUID, Name, Type | ERROR | +| 8 | Уникальность имён | ERROR | +| 9 | TabularSections — внутренняя структура | ERROR / WARN | +| 10 | Кросс-свойства | ERROR / WARN | +| 11 | HTTPService/WebService — вложенная структура | ERROR | + +## Вывод + +``` +=== Validation: Catalog.Номенклатура === + +[OK] 1. Root structure: MetaDataObject/Catalog, version 2.17 +[OK] 2. InternalInfo: 5 GeneratedType (Object, Ref, Selection, List, Manager) +[OK] 3. Properties: Name="Номенклатура", Synonym present +[OK] 4. Property values: 12 enum properties checked +[ERROR] 5. StandardAttributes: missing "PredefinedDataName" +[OK] 6. ChildObjects types: Attribute(15), TabularSection(3), Form(4) +[OK] 7. Attributes/Dimensions: all valid +[WARN] 8. Name uniqueness: duplicate attribute "Комментарий" at positions 5, 12 +[OK] 9. TabularSections: 3 sections, structure valid +[OK] 10. Cross-property consistency +[OK] 11. N/A (not HTTPService/WebService) +--- +Errors: 1, Warnings: 1 +``` + +Код возврата: 0 = все проверки пройдены, 1 = есть ошибки. + +## Примеры + +```powershell +# Справочник из выгрузки конфигурации +... -ObjectPath upload/acc_8.3.24/Catalogs/Банки/Банки.xml + +# Авторезолв из директории +... -ObjectPath upload/acc_8.3.24/Documents/АвансовыйОтчет + +# С лимитом ошибок +... -ObjectPath Catalogs/Номенклатура.xml -MaxErrors 10 + +# С записью в файл +... -ObjectPath Catalogs/Номенклатура.xml -OutFile result.txt +``` + +## Верификация + +``` +/meta-compile — генерация XML +/meta-validate //.xml — проверка результата +/meta-info //.xml — визуальная сводка +``` + +## Когда использовать + +- **После `/meta-compile`**: проверить корректность сгенерированного XML +- **После ручного редактирования**: убедиться что структура не нарушена +- **После merge/импорта**: выявить конфликты и битые ссылки +- **При отладке**: найти структурные ошибки до сборки EPF diff --git a/.claude/skills/meta-validate/scripts/meta-validate.ps1 b/.claude/skills/meta-validate/scripts/meta-validate.ps1 new file mode 100644 index 00000000..436f01e6 --- /dev/null +++ b/.claude/skills/meta-validate/scripts/meta-validate.ps1 @@ -0,0 +1,984 @@ +# meta-validate v1.0 — Validate 1C metadata object structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ObjectPath, + + [int]$MaxErrors = 30, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- + +if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) { + $ObjectPath = Join-Path (Get-Location).Path $ObjectPath +} + +if (Test-Path $ObjectPath -PathType Container) { + $dirName = Split-Path $ObjectPath -Leaf + $candidate = Join-Path $ObjectPath "$dirName.xml" + if (Test-Path $candidate) { + $ObjectPath = $candidate + } else { + $xmlFiles = @(Get-ChildItem $ObjectPath -Filter "*.xml" -File | Select-Object -First 1) + if ($xmlFiles.Count -gt 0) { + $ObjectPath = $xmlFiles[0].FullName + } else { + Write-Host "[ERROR] No XML file found in directory: $ObjectPath" + exit 1 + } + } +} + +if (-not (Test-Path $ObjectPath)) { + Write-Host "[ERROR] File not found: $ObjectPath" + exit 1 +} + +$resolvedPath = (Resolve-Path $ObjectPath).Path + +# --- Output infrastructure --- + +$script:errors = 0 +$script:warnings = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 8192 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + Out-Line "[OK] $msg" +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ===" + + $result = $script:output.ToString() + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +# --- Reference tables --- + +$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' +$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' + +$validTypes = @( + "Catalog","Document","Enum","Constant", + "InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister", + "ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes", + "BusinessProcess","Task","ExchangePlan","DocumentJournal", + "Report","DataProcessor", + "CommonModule","ScheduledJob","EventSubscription", + "HTTPService","WebService","DefinedType" +) + +# GeneratedType categories by type +$generatedTypeCategories = @{ + "Catalog" = @("Object","Ref","Selection","List","Manager") + "Document" = @("Object","Ref","Selection","List","Manager") + "Enum" = @("Ref","Manager","List") + "Constant" = @("Manager","ValueManager","ValueKey") + "InformationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","RecordManager") + "AccumulationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey") + "AccountingRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","ExtDimensions") + "CalculationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey") + "ChartOfAccounts" = @("Object","Ref","Selection","List","Manager","ExtDimensionTypes","ExtDimensionTypesRow") + "ChartOfCharacteristicTypes" = @("Object","Ref","Selection","List","Manager") + "ChartOfCalculationTypes" = @("Object","Ref","Selection","List","Manager","DisplacingCalculationTypes","BaseCalculationTypes","LeadingCalculationTypes") + "BusinessProcess" = @("Object","Ref","Selection","List","Manager") + "Task" = @("Object","Ref","Selection","List","Manager") + "ExchangePlan" = @("Object","Ref","Selection","List","Manager") + "DocumentJournal" = @("Selection","List","Manager") + "Report" = @("Object") + "DataProcessor" = @("Object") +} + +# Types that have NO InternalInfo / GeneratedType +$typesWithoutInternalInfo = @("CommonModule","ScheduledJob","EventSubscription","DefinedType") + +# StandardAttributes by type +$standardAttributesByType = @{ + "Catalog" = @("PredefinedDataName","Predefined","Ref","DeletionMark","IsFolder","Owner","Parent","Description","Code") + "Document" = @("Posted","Ref","DeletionMark","Date","Number") + "Enum" = @("Order","Ref") + "InformationRegister" = @("Active","LineNumber","Recorder","Period") + "AccumulationRegister" = @("Active","LineNumber","Recorder","Period","RecordType") + "AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account","PeriodAdjustment") + "CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry") + "ChartOfAccounts" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","Order","Type","OffBalance") + "ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","ValueType") + "ChartOfCalculationTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","ActionPeriodIsBasic") + "BusinessProcess" = @("Ref","DeletionMark","Date","Number","Started","Completed","HeadTask") + "Task" = @("Ref","DeletionMark","Date","Number","Executed","Description","RoutePoint","BusinessProcess") + "ExchangePlan" = @("Ref","DeletionMark","Code","Description","ThisNode","SentNo","ReceivedNo") + "DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number") +} + +# Types that have StandardAttributes block +$typesWithStdAttrs = @( + "Catalog","Document","Enum", + "InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister", + "ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes", + "BusinessProcess","Task","ExchangePlan","DocumentJournal" +) + +# ChildObjects rules: what child element types are valid for each metadata type +$childObjectRules = @{ + "Catalog" = @("Attribute","TabularSection","Form","Template","Command") + "Document" = @("Attribute","TabularSection","Form","Template","Command") + "ExchangePlan" = @("Attribute","TabularSection","Form","Template","Command") + "ChartOfAccounts" = @("Attribute","TabularSection","Form","Template","Command","AccountingFlag","ExtDimensionAccountingFlag") + "ChartOfCharacteristicTypes" = @("Attribute","TabularSection","Form","Template","Command") + "ChartOfCalculationTypes" = @("Attribute","TabularSection","Form","Template","Command") + "BusinessProcess" = @("Attribute","TabularSection","Form","Template","Command") + "Task" = @("Attribute","TabularSection","Form","Template","Command","AddressingAttribute") + "Report" = @("Attribute","TabularSection","Form","Template","Command") + "DataProcessor" = @("Attribute","TabularSection","Form","Template","Command") + "Enum" = @("EnumValue","Form","Template","Command") + "InformationRegister" = @("Dimension","Resource","Attribute","Form","Template","Command") + "AccumulationRegister" = @("Dimension","Resource","Attribute","Form","Template","Command") + "AccountingRegister" = @("Dimension","Resource","Attribute","Form","Template","Command") + "CalculationRegister" = @("Dimension","Resource","Attribute","Form","Template","Command","Recalculation") + "DocumentJournal" = @("Column","Form","Template","Command") + "HTTPService" = @("URLTemplate") + "WebService" = @("Operation") + "Constant" = @("Form") + "DefinedType" = @() + "CommonModule" = @() + "ScheduledJob" = @() + "EventSubscription" = @() +} + +# Valid enum property values +$validPropertyValues = @{ + "CodeType" = @("String","Number") + "CodeAllowedLength" = @("Variable","Fixed") + "NumberType" = @("String","Number") + "NumberAllowedLength" = @("Variable","Fixed") + "Posting" = @("Allow","Deny") + "RealTimePosting" = @("Allow","Deny") + "RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff") + "RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll") + "DataLockControlMode" = @("Automatic","Managed") + "FullTextSearch" = @("Use","DontUse") + "DefaultPresentation" = @("AsDescription","AsCode") + "HierarchyType" = @("HierarchyFoldersAndItems","HierarchyItemsOnly") + "EditType" = @("InDialog","InList","BothWays") + "WriteMode" = @("Independent","RecorderSubordinate") + "InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year") + "RegisterType" = @("Balance","Turnovers") + "ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession") + "ReuseSessions" = @("DontUse","AutoUse") + "FillChecking" = @("DontCheck","ShowError","ShowWarning") + "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") + "DataHistory" = @("Use","DontUse") +} + +# --- 1. Parse XML --- + +Out-Line "" + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) +} catch { + Out-Line "=== Validation: (parse failed) ===" + Out-Line "" + Report-Error "1. XML parse failed: $($_.Exception.Message)" + & $finalize + exit 1 +} + +# --- 2. Register namespaces --- + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("cfg", "http://v8.1c.ru/8.1/data/enterprise/current-config") + +$root = $xmlDoc.DocumentElement + +# --- Check 1: Root structure --- + +$check1Ok = $true + +# Root must be MetaDataObject +if ($root.LocalName -ne "MetaDataObject") { + Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" + & $finalize + exit 1 +} + +$expectedNs = "http://v8.1c.ru/8.3/MDClasses" +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" + $check1Ok = $false +} + +# Version attribute +$version = $root.GetAttribute("version") +if (-not $version) { + Report-Warn "1. Missing version attribute on MetaDataObject" +} elseif ($version -ne "2.17" -and $version -ne "2.20") { + Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)" +} + +# Detect type element — exactly one child element in md namespace +$typeNode = $null +$mdType = "" +$childElements = @() +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.NamespaceURI -eq $expectedNs) { + $childElements += $child + } +} + +if ($childElements.Count -eq 0) { + Report-Error "1. No metadata type element found inside MetaDataObject" + & $finalize + exit 1 +} elseif ($childElements.Count -gt 1) { + Report-Error "1. Multiple type elements found: $($childElements | ForEach-Object { $_.LocalName })" + $check1Ok = $false +} + +$typeNode = $childElements[0] +$mdType = $typeNode.LocalName + +if ($validTypes -notcontains $mdType) { + Report-Error "1. Unrecognized metadata type: $mdType" + & $finalize + exit 1 +} + +# UUID on type element +$typeUuid = $typeNode.GetAttribute("uuid") +if (-not $typeUuid) { + Report-Error "1. Missing uuid on <$mdType> element" + $check1Ok = $false +} elseif ($typeUuid -notmatch $guidPattern) { + Report-Error "1. Invalid uuid '$typeUuid' on <$mdType>" + $check1Ok = $false +} + +# Get object name early for header +$propsNode = $typeNode.SelectSingleNode("md:Properties", $ns) +$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } +$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } + +# Now emit header +$script:output.Insert(0, "=== Validation: $mdType.$objName ===$([Environment]::NewLine)") | Out-Null + +if ($check1Ok) { + Report-OK "1. Root structure: MetaDataObject/$mdType, version $version" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 2: InternalInfo --- + +$internalInfo = $typeNode.SelectSingleNode("md:InternalInfo", $ns) + +if ($typesWithoutInternalInfo -contains $mdType) { + # These types should NOT have InternalInfo with GeneratedType + if ($internalInfo) { + $genTypes = $internalInfo.SelectNodes("xr:GeneratedType", $ns) + if ($genTypes.Count -gt 0) { + Report-Warn "2. InternalInfo: $mdType should not have GeneratedType entries, found $($genTypes.Count)" + } else { + Report-OK "2. InternalInfo: absent or empty (correct for $mdType)" + } + } else { + Report-OK "2. InternalInfo: absent (correct for $mdType)" + } +} elseif ($generatedTypeCategories.ContainsKey($mdType)) { + $expectedCategories = $generatedTypeCategories[$mdType] + if (-not $internalInfo) { + Report-Error "2. InternalInfo: missing (expected $($expectedCategories.Count) GeneratedType)" + } else { + $genTypes = $internalInfo.SelectNodes("xr:GeneratedType", $ns) + $check2Ok = $true + $foundCategories = @() + + foreach ($gt in $genTypes) { + $gtName = $gt.GetAttribute("name") + $gtCategory = $gt.GetAttribute("category") + $foundCategories += $gtCategory + + # Validate name format: Prefix.ObjectName + if ($gtName -and $objName -ne "(unknown)") { + if (-not $gtName.EndsWith(".$objName")) { + Report-Error "2. GeneratedType name '$gtName' does not end with '.$objName'" + $check2Ok = $false + } + } + + # Validate category + if ($expectedCategories -notcontains $gtCategory) { + Report-Warn "2. Unexpected GeneratedType category '$gtCategory' for $mdType" + } + + # Validate TypeId and ValueId UUIDs + $typeId = $gt.SelectSingleNode("xr:TypeId", $ns) + $valueId = $gt.SelectSingleNode("xr:ValueId", $ns) + if ($typeId -and $typeId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid TypeId UUID in GeneratedType '$gtCategory'" + $check2Ok = $false + } + if ($valueId -and $valueId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ValueId UUID in GeneratedType '$gtCategory'" + $check2Ok = $false + } + } + + # ExchangePlan: check for ThisNode + if ($mdType -eq "ExchangePlan") { + $thisNode = $internalInfo.SelectSingleNode("xr:ThisNode", $ns) + if (-not $thisNode) { + Report-Warn "2. ExchangePlan missing xr:ThisNode in InternalInfo" + } elseif ($thisNode.InnerText -notmatch $guidPattern) { + Report-Error "2. ExchangePlan xr:ThisNode has invalid UUID" + $check2Ok = $false + } + } + + # Check count mismatch + $missingCats = @($expectedCategories | Where-Object { $foundCategories -notcontains $_ }) + if ($missingCats.Count -gt 0) { + Report-Warn "2. Missing GeneratedType categories: $($missingCats -join ', ')" + } + + if ($check2Ok) { + $catList = ($foundCategories | Sort-Object) -join ", " + Report-OK "2. InternalInfo: $($genTypes.Count) GeneratedType ($catList)" + } + } +} else { + Report-OK "2. InternalInfo: N/A for $mdType" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 3: Properties — Name, Synonym --- + +if (-not $propsNode) { + Report-Error "3. Properties block missing" +} else { + $check3Ok = $true + + # Name + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "3. Properties: Name is missing or empty" + $check3Ok = $false + } else { + $nameVal = $nameNode.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "3. Properties: Name '$nameVal' is not a valid 1C identifier" + $check3Ok = $false + } + if ($nameVal.Length -gt 80) { + Report-Warn "3. Properties: Name '$nameVal' is longer than 80 characters ($($nameVal.Length))" + } + } + + # Synonym + $synNode = $propsNode.SelectSingleNode("md:Synonym", $ns) + $synPresent = $false + if ($synNode) { + $synItem = $synNode.SelectSingleNode("v8:item", $ns) + if ($synItem) { + $synContent = $synItem.SelectSingleNode("v8:content", $ns) + if ($synContent -and $synContent.InnerText) { + $synPresent = $true + } + } + } + + if ($check3Ok) { + $synInfo = if ($synPresent) { "Synonym present" } else { "no Synonym" } + Report-OK "3. Properties: Name=`"$objName`", $synInfo" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 4: Property values — enum properties --- + +if ($propsNode) { + $enumChecked = 0 + $check4Ok = $true + + foreach ($propName in $validPropertyValues.Keys) { + $propNode = $propsNode.SelectSingleNode("md:$propName", $ns) + if ($propNode -and $propNode.InnerText) { + $val = $propNode.InnerText + $allowed = $validPropertyValues[$propName] + if ($allowed -notcontains $val) { + Report-Error "4. Property '$propName' has invalid value '$val' (allowed: $($allowed -join ', '))" + $check4Ok = $false + } + $enumChecked++ + } + } + + if ($check4Ok) { + Report-OK "4. Property values: $enumChecked enum properties checked" + } +} else { + Report-Warn "4. No Properties block to check" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 5: StandardAttributes --- + +if ($typesWithStdAttrs -contains $mdType) { + $stdAttrNode = $propsNode.SelectSingleNode("md:StandardAttributes", $ns) + if (-not $stdAttrNode) { + Report-Error "5. StandardAttributes block missing for $mdType" + } else { + $stdAttrs = $stdAttrNode.SelectNodes("xr:StandardAttribute", $ns) + $expectedStdAttrs = $standardAttributesByType[$mdType] + $check5Ok = $true + + $foundNames = @() + foreach ($sa in $stdAttrs) { + $saName = $sa.GetAttribute("name") + if ($saName) { + $foundNames += $saName + if ($expectedStdAttrs -notcontains $saName) { + # AccountingRegister has dynamic ExtDimension{N}/ExtDimensionType{N} attrs + $isDynamic = ($mdType -eq "AccountingRegister" -and ($saName -match '^ExtDimension\d+$' -or $saName -match '^ExtDimensionType\d+$')) + if (-not $isDynamic) { + Report-Warn "5. Unexpected StandardAttribute '$saName' for $mdType" + } + } + } else { + Report-Error "5. StandardAttribute without 'name' attribute" + $check5Ok = $false + } + } + + if ($expectedStdAttrs) { + $missingAttrs = @($expectedStdAttrs | Where-Object { $foundNames -notcontains $_ }) + if ($missingAttrs.Count -gt 0) { + Report-Warn "5. Missing StandardAttributes: $($missingAttrs -join ', ')" + } + } + + if ($check5Ok) { + Report-OK "5. StandardAttributes: $($stdAttrs.Count) entries" + } + } +} else { + Report-OK "5. StandardAttributes: N/A for $mdType" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 6: ChildObjects — allowed element types --- + +$childObjNode = $typeNode.SelectSingleNode("md:ChildObjects", $ns) +$allowedChildren = $childObjectRules[$mdType] + +if ($childObjNode) { + $check6Ok = $true + $childCounts = @{} + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childTag = $child.LocalName + + if ($allowedChildren -notcontains $childTag) { + Report-Error "6. ChildObjects: disallowed element '$childTag' for $mdType" + $check6Ok = $false + } + + if (-not $childCounts.ContainsKey($childTag)) { + $childCounts[$childTag] = 0 + } + $childCounts[$childTag]++ + } + + if ($check6Ok) { + $summary = ($childCounts.GetEnumerator() | Sort-Object Name | ForEach-Object { "$($_.Name)($($_.Value))" }) -join ", " + if ($summary) { + Report-OK "6. ChildObjects types: $summary" + } else { + Report-OK "6. ChildObjects: empty (valid for $mdType)" + } + } +} elseif ($allowedChildren.Count -eq 0) { + Report-OK "6. ChildObjects: absent (correct for $mdType)" +} else { + # Some types may have no children — that's OK + Report-OK "6. ChildObjects: absent" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 7: Attributes/Dimensions/Resources/EnumValues/Columns — UUID, Name, Type --- + +function Check-ChildElement { + param( + [System.Xml.XmlNode]$node, + [string]$kind, + [bool]$requireType + ) + + $uuid = $node.GetAttribute("uuid") + if (-not $uuid) { + Report-Error "7. $kind missing uuid" + return $false + } elseif ($uuid -notmatch $guidPattern) { + Report-Error "7. $kind has invalid uuid '$uuid'" + return $false + } + + $elProps = $node.SelectSingleNode("md:Properties", $ns) + if (-not $elProps) { + Report-Error "7. $kind (uuid=$uuid) missing Properties" + return $false + } + + $elName = $elProps.SelectSingleNode("md:Name", $ns) + if (-not $elName -or -not $elName.InnerText) { + Report-Error "7. $kind (uuid=$uuid) missing or empty Name" + return $false + } + + $nameVal = $elName.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "7. $kind '$nameVal' has invalid identifier" + return $false + } + + if ($requireType) { + $typeEl = $elProps.SelectSingleNode("md:Type", $ns) + if (-not $typeEl) { + Report-Error "7. $kind '$nameVal' missing Type block" + return $false + } + $v8Types = $typeEl.SelectNodes("v8:Type", $ns) + $v8TypeSets = $typeEl.SelectNodes("v8:TypeSet", $ns) + if ($v8Types.Count -eq 0 -and $v8TypeSets.Count -eq 0) { + Report-Error "7. $kind '$nameVal' Type block has no v8:Type or v8:TypeSet" + return $false + } + } + + return $true +} + +if ($childObjNode) { + $check7Ok = $true + $check7Count = 0 + $elementKinds = @("Attribute","Dimension","Resource","EnumValue","Column") + + foreach ($kind in $elementKinds) { + $elements = $childObjNode.SelectNodes("md:$kind", $ns) + $requireType = ($kind -ne "EnumValue") + foreach ($el in $elements) { + if ($script:stopped) { break } + $ok = Check-ChildElement -node $el -kind $kind -requireType $requireType + if (-not $ok) { $check7Ok = $false } + $check7Count++ + } + } + + if ($check7Ok -and $check7Count -gt 0) { + Report-OK "7. Child elements: $check7Count items checked (UUID, Name, Type)" + } elseif ($check7Count -eq 0) { + Report-OK "7. Child elements: none to check" + } +} else { + Report-OK "7. Child elements: N/A (no ChildObjects)" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 8: Name uniqueness --- + +function Check-Uniqueness { + param( + [System.Xml.XmlNodeList]$nodes, + [string]$kind + ) + + $names = @{} + $hasDupes = $false + + foreach ($node in $nodes) { + $elProps = $node.SelectSingleNode("md:Properties", $ns) + if (-not $elProps) { continue } + $elName = $elProps.SelectSingleNode("md:Name", $ns) + if (-not $elName -or -not $elName.InnerText) { continue } + + $nameVal = $elName.InnerText + if ($names.ContainsKey($nameVal)) { + Report-Error "8. Duplicate $kind name: '$nameVal'" + $hasDupes = $true + } else { + $names[$nameVal] = $true + } + } + + return (-not $hasDupes) +} + +if ($childObjNode) { + $check8Ok = $true + + # Attributes + $attrs = $childObjNode.SelectNodes("md:Attribute", $ns) + if ($attrs.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $attrs -kind "Attribute")) { $check8Ok = $false } + } + + # TabularSections + $tss = $childObjNode.SelectNodes("md:TabularSection", $ns) + if ($tss.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $tss -kind "TabularSection")) { $check8Ok = $false } + } + + # Dimensions + $dims = $childObjNode.SelectNodes("md:Dimension", $ns) + if ($dims.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $dims -kind "Dimension")) { $check8Ok = $false } + } + + # Resources + $ress = $childObjNode.SelectNodes("md:Resource", $ns) + if ($ress.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $ress -kind "Resource")) { $check8Ok = $false } + } + + # EnumValues + $evs = $childObjNode.SelectNodes("md:EnumValue", $ns) + if ($evs.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $evs -kind "EnumValue")) { $check8Ok = $false } + } + + # Columns (DocumentJournal) + $cols = $childObjNode.SelectNodes("md:Column", $ns) + if ($cols.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $cols -kind "Column")) { $check8Ok = $false } + } + + # URLTemplates (HTTPService) + $urlTs = $childObjNode.SelectNodes("md:URLTemplate", $ns) + if ($urlTs.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $urlTs -kind "URLTemplate")) { $check8Ok = $false } + } + + # Operations (WebService) + $ops = $childObjNode.SelectNodes("md:Operation", $ns) + if ($ops.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $ops -kind "Operation")) { $check8Ok = $false } + } + + if ($check8Ok) { + Report-OK "8. Name uniqueness: all names unique" + } +} else { + Report-OK "8. Name uniqueness: N/A" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 9: TabularSections — internal structure --- + +if ($childObjNode) { + $tsSections = $childObjNode.SelectNodes("md:TabularSection", $ns) + if ($tsSections.Count -gt 0) { + $check9Ok = $true + $tsCount = 0 + + foreach ($ts in $tsSections) { + if ($script:stopped) { break } + $tsCount++ + + # UUID + $tsUuid = $ts.GetAttribute("uuid") + if (-not $tsUuid -or $tsUuid -notmatch $guidPattern) { + Report-Error "9. TabularSection #${tsCount}: invalid or missing uuid" + $check9Ok = $false + } + + # Name + $tsProps = $ts.SelectSingleNode("md:Properties", $ns) + $tsNameNode = if ($tsProps) { $tsProps.SelectSingleNode("md:Name", $ns) } else { $null } + $tsName = if ($tsNameNode) { $tsNameNode.InnerText } else { "(unnamed)" } + + if (-not $tsNameNode -or -not $tsNameNode.InnerText) { + Report-Error "9. TabularSection #${tsCount}: missing or empty Name" + $check9Ok = $false + } + + # InternalInfo with 2 GeneratedType (TabularSection + TabularSectionRow) + $tsIntInfo = $ts.SelectSingleNode("md:InternalInfo", $ns) + if ($tsIntInfo) { + $tsGens = $tsIntInfo.SelectNodes("xr:GeneratedType", $ns) + if ($tsGens.Count -lt 2) { + Report-Warn "9. TabularSection '$tsName': expected 2 GeneratedType, found $($tsGens.Count)" + } + } + + # Attributes inside TS + $tsChildObj = $ts.SelectSingleNode("md:ChildObjects", $ns) + if ($tsChildObj) { + $tsAttrs = $tsChildObj.SelectNodes("md:Attribute", $ns) + $tsAttrNames = @{} + foreach ($ta in $tsAttrs) { + $taOk = Check-ChildElement -node $ta -kind "TabularSection '$tsName'.Attribute" -requireType $true + if (-not $taOk) { $check9Ok = $false } + + # Check name uniqueness within TS + $taProps = $ta.SelectSingleNode("md:Properties", $ns) + $taName = if ($taProps) { $taProps.SelectSingleNode("md:Name", $ns) } else { $null } + if ($taName -and $taName.InnerText) { + if ($tsAttrNames.ContainsKey($taName.InnerText)) { + Report-Error "9. Duplicate attribute '$($taName.InnerText)' in TabularSection '$tsName'" + $check9Ok = $false + } else { + $tsAttrNames[$taName.InnerText] = $true + } + } + } + + # StandardAttributes of TS: expect LineNumber + $tsStdAttr = $tsProps.SelectSingleNode("md:StandardAttributes", $ns) + if ($tsStdAttr) { + $tsStdAttrs = $tsStdAttr.SelectNodes("xr:StandardAttribute", $ns) + $hasLineNumber = $false + foreach ($tsa in $tsStdAttrs) { + if ($tsa.GetAttribute("name") -eq "LineNumber") { $hasLineNumber = $true } + } + if (-not $hasLineNumber) { + Report-Warn "9. TabularSection '$tsName': missing LineNumber StandardAttribute" + } + } + } + } + + if ($check9Ok) { + Report-OK "9. TabularSections: $tsCount sections, structure valid" + } + } else { + Report-OK "9. TabularSections: none present" + } +} else { + Report-OK "9. TabularSections: N/A" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 10: Cross-property consistency --- + +$check10Ok = $true +$check10Issues = 0 + +if ($propsNode) { + # HierarchyType set but Hierarchical = false + $hierarchical = $propsNode.SelectSingleNode("md:Hierarchical", $ns) + $hierarchyType = $propsNode.SelectSingleNode("md:HierarchyType", $ns) + if ($hierarchical -and $hierarchyType -and $hierarchical.InnerText -eq "false" -and $hierarchyType.InnerText) { + Report-Warn "10. HierarchyType='$($hierarchyType.InnerText)' but Hierarchical=false" + $check10Issues++ + } + + # CommonModule: no context enabled + if ($mdType -eq "CommonModule") { + $contexts = @("Server","ClientManagedApplication","ClientOrdinaryApplication","ExternalConnection","ServerCall","Global") + $anyEnabled = $false + foreach ($ctx in $contexts) { + $ctxNode = $propsNode.SelectSingleNode("md:$ctx", $ns) + if ($ctxNode -and $ctxNode.InnerText -eq "true") { + $anyEnabled = $true + break + } + } + if (-not $anyEnabled) { + Report-Warn "10. CommonModule: no execution context enabled" + $check10Issues++ + } + } + + # EventSubscription: empty Handler + if ($mdType -eq "EventSubscription") { + $handler = $propsNode.SelectSingleNode("md:Handler", $ns) + if (-not $handler -or -not $handler.InnerText.Trim()) { + Report-Error "10. EventSubscription: empty Handler" + $check10Ok = $false + $check10Issues++ + } + + # Empty Source + $source = $propsNode.SelectSingleNode("md:Source", $ns) + $hasSource = $false + if ($source) { + $sourceTypes = $source.SelectNodes("v8:Type", $ns) + if ($sourceTypes.Count -gt 0) { $hasSource = $true } + } + if (-not $hasSource) { + Report-Warn "10. EventSubscription: no Source types specified" + $check10Issues++ + } + } + + # ScheduledJob: empty MethodName + if ($mdType -eq "ScheduledJob") { + $method = $propsNode.SelectSingleNode("md:MethodName", $ns) + if (-not $method -or -not $method.InnerText.Trim()) { + Report-Error "10. ScheduledJob: empty MethodName" + $check10Ok = $false + $check10Issues++ + } + } +} + +if ($check10Ok -and $check10Issues -eq 0) { + Report-OK "10. Cross-property consistency" +} elseif ($check10Ok) { + # Had warnings but no errors — already reported +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 11: HTTPService/WebService nested structure --- + +if ($mdType -eq "HTTPService" -and $childObjNode) { + $urlTemplates = $childObjNode.SelectNodes("md:URLTemplate", $ns) + $check11Ok = $true + $methodCount = 0 + + $validHTTPMethods = @("GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS","MERGE","CONNECT") + + foreach ($ut in $urlTemplates) { + if ($script:stopped) { break } + + $utProps = $ut.SelectSingleNode("md:Properties", $ns) + $utNameNode = if ($utProps) { $utProps.SelectSingleNode("md:Name", $ns) } else { $null } + $utName = if ($utNameNode) { $utNameNode.InnerText } else { "(unnamed)" } + + # Template property + $tpl = if ($utProps) { $utProps.SelectSingleNode("md:Template", $ns) } else { $null } + if (-not $tpl -or -not $tpl.InnerText.Trim()) { + Report-Error "11. HTTPService URLTemplate '$utName': empty Template" + $check11Ok = $false + } + + # Methods inside URLTemplate + $utChildObj = $ut.SelectSingleNode("md:ChildObjects", $ns) + if ($utChildObj) { + $methods = $utChildObj.SelectNodes("md:Method", $ns) + foreach ($m in $methods) { + $methodCount++ + $mProps = $m.SelectSingleNode("md:Properties", $ns) + if ($mProps) { + $httpMethod = $mProps.SelectSingleNode("md:HTTPMethod", $ns) + if ($httpMethod -and $httpMethod.InnerText) { + if ($validHTTPMethods -notcontains $httpMethod.InnerText) { + Report-Error "11. HTTPService URLTemplate '$utName': invalid HTTPMethod '$($httpMethod.InnerText)'" + $check11Ok = $false + } + } else { + Report-Error "11. HTTPService URLTemplate '$utName': Method missing HTTPMethod" + $check11Ok = $false + } + } + } + } + } + + if ($check11Ok) { + Report-OK "11. HTTPService: $($urlTemplates.Count) URLTemplate(s), $methodCount method(s)" + } +} elseif ($mdType -eq "WebService" -and $childObjNode) { + $operations = $childObjNode.SelectNodes("md:Operation", $ns) + $check11Ok = $true + $paramCount = 0 + + $validDirections = @("In","Out","InOut") + + foreach ($op in $operations) { + if ($script:stopped) { break } + + $opProps = $op.SelectSingleNode("md:Properties", $ns) + $opNameNode = if ($opProps) { $opProps.SelectSingleNode("md:Name", $ns) } else { $null } + $opName = if ($opNameNode) { $opNameNode.InnerText } else { "(unnamed)" } + + # ReturnType — XDTOReturningValueType + $retType = if ($opProps) { $opProps.SelectSingleNode("md:XDTOReturningValueType", $ns) } else { $null } + if (-not $retType -or -not $retType.InnerText.Trim()) { + Report-Warn "11. WebService Operation '$opName': no XDTOReturningValueType" + } + + # Parameters inside Operation + $opChildObj = $op.SelectSingleNode("md:ChildObjects", $ns) + if ($opChildObj) { + $params = $opChildObj.SelectNodes("md:Parameter", $ns) + foreach ($p in $params) { + $paramCount++ + $pProps = $p.SelectSingleNode("md:Properties", $ns) + if ($pProps) { + $dir = $pProps.SelectSingleNode("md:TransferDirection", $ns) + if ($dir -and $dir.InnerText -and $validDirections -notcontains $dir.InnerText) { + Report-Error "11. WebService Operation '$opName': Parameter has invalid TransferDirection '$($dir.InnerText)'" + $check11Ok = $false + } + } + } + } + } + + if ($check11Ok) { + Report-OK "11. WebService: $($operations.Count) operation(s), $paramCount parameter(s)" + } +} else { + Report-OK "11. HTTPService/WebService: N/A" +} + +# --- Final output --- + +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0