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 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-02-14 13:00:05 +03:00
parent ed45f54594
commit a6bf6520ce
2 changed files with 1096 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
---
name: meta-validate
description: Валидация структурной корректности объекта метаданных 1С (Справочник, Документ, Регистр, Перечисление и ещё 19 типов)
argument-hint: <ObjectPath> [-MaxErrors 30]
allowed-tools:
- Bash
- Read
- Glob
---
# /meta-validate — валидация объекта метаданных 1С
Проверяет XML объекта метаданных из выгрузки конфигурации на структурные ошибки: корневую структуру, InternalInfo, свойства, допустимые значения, StandardAttributes, ChildObjects, уникальность имён, табличные части, кросс-свойства, вложенные структуры HTTP/Web-сервисов.
## Использование
```
/meta-validate <ObjectPath>
```
## Параметры
| Параметр | Обязательный | По умолчанию | Описание |
|------------|:------------:|--------------|-------------------------------------------------|
| ObjectPath | да | — | Путь к XML-файлу или каталогу объекта |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
`ObjectPath` авторезолв: если указана директория — ищет `<dirName>/<dirName>.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 <JsonPath> <OutputDir> — генерация XML
/meta-validate <OutputDir>/<Type>/<Name>.xml — проверка результата
/meta-info <OutputDir>/<Type>/<Name>.xml — визуальная сводка
```
## Когда использовать
- **После `/meta-compile`**: проверить корректность сгенерированного XML
- **После ручного редактирования**: убедиться что структура не нарушена
- **После merge/импорта**: выявить конфликты и битые ссылки
- **При отладке**: найти структурные ошибки до сборки EPF
@@ -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