# meta-validate v1.3 — Validate 1C metadata object structure # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] [Alias('Path')] [string]$ObjectPath, [switch]$Detailed, [int]$MaxErrors = 30, [string]$OutFile ) $ErrorActionPreference = "Stop" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # --- Batch mode: pipe-separated paths (comma reserved by PowerShell) --- $pathList = @($ObjectPath -split '\|' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) if ($pathList.Count -gt 1) { $batchOk = 0 $batchFail = 0 foreach ($singlePath in $pathList) { $callArgs = @{ ObjectPath = $singlePath; MaxErrors = $MaxErrors; Verbose = $Detailed } if ($OutFile) { $baseName = [System.IO.Path]::GetFileNameWithoutExtension($OutFile) $ext = [System.IO.Path]::GetExtension($OutFile) $dir = Split-Path $OutFile if (-not $dir) { $dir = "." } $objLeaf = [System.IO.Path]::GetFileNameWithoutExtension($singlePath) $callArgs.OutFile = Join-Path $dir "$baseName`_$objLeaf$ext" } & $PSCommandPath @callArgs if ($LASTEXITCODE -eq 0) { $batchOk++ } else { $batchFail++ } } Write-Host "" Write-Host "=== Batch: $($pathList.Count) objects, $batchOk passed, $batchFail failed ===" if ($batchFail -gt 0) { exit 1 } exit 0 } # --- 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" $sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml" if (Test-Path $candidate) { $ObjectPath = $candidate } elseif (Test-Path $sibling) { $ObjectPath = $sibling } 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 } } } # File not found — check Dir/Name/Name.xml → Dir/Name.xml if (-not (Test-Path $ObjectPath)) { $fileName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath) $parentDir = Split-Path $ObjectPath $parentDirName = Split-Path $parentDir -Leaf if ($fileName -eq $parentDirName) { $candidate = Join-Path (Split-Path $parentDir) "$fileName.xml" if (Test-Path $candidate) { $ObjectPath = $candidate } } } if (-not (Test-Path $ObjectPath)) { Write-Host "[ERROR] File not found: $ObjectPath" exit 1 } $resolvedPath = (Resolve-Path $ObjectPath).Path # --- Detect config directory (for cross-object checks) --- $script:configDir = $null $probe = Split-Path $resolvedPath for ($depth = 0; $depth -lt 4; $depth++) { if (-not $probe) { break } if (Test-Path (Join-Path $probe "Configuration.xml")) { $script:configDir = $probe break } $probe = Split-Path $probe } # --- Output infrastructure --- $script:errors = 0 $script:warnings = 0 $script:okCount = 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) $script:okCount++ if ($Detailed) { 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 = { $checks = $script:okCount + $script:errors + $script:warnings if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { $result = "=== Validation OK: $mdType.$objName ($checks checks) ===" } else { Out-Line "" Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" $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","Recalcs") "ChartOfAccounts" = @("Object","Ref","Selection","List","Manager","ExtDimensionTypes","ExtDimensionTypesRow") "ChartOfCharacteristicTypes" = @("Object","Ref","Selection","List","Manager","Characteristic") "ChartOfCalculationTypes" = @("Object","Ref","Selection","List","Manager","DisplacingCalculationTypes","DisplacingCalculationTypesRow","BaseCalculationTypes","BaseCalculationTypesRow","LeadingCalculationTypes","LeadingCalculationTypesRow") "BusinessProcess" = @("Object","Ref","Selection","List","Manager","RoutePointRef") "Task" = @("Object","Ref","Selection","List","Manager") "ExchangePlan" = @("Object","Ref","Selection","List","Manager") "DocumentJournal" = @("Selection","List","Manager") "Report" = @("Object","Manager") "DataProcessor" = @("Object","Manager") "DefinedType" = @("DefinedType") } # Types that have NO InternalInfo / GeneratedType $typesWithoutInternalInfo = @("CommonModule","ScheduledJob","EventSubscription") # 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") "CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry","ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod") "ChartOfAccounts" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","Order","Type","OffBalance") "ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","IsFolder","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","RecorderPosition") "RegisterType" = @("Balance","Turnovers") "ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession") "ReuseSessions" = @("DontUse","AutoUse") "FillChecking" = @("DontCheck","ShowError","ShowWarning") "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") "DataHistory" = @("Use","DontUse") "DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod") } # Properties forbidden per type (would cause LoadConfigFromFiles error) $forbiddenProperties = @{ "ChartOfCharacteristicTypes" = @("CodeType") "ChartOfAccounts" = @("Autonumbering","Hierarchical") "ChartOfCalculationTypes" = @("CheckUnique","Autonumbering") "ExchangePlan" = @("CodeType","CheckUnique","Autonumbering") } # --- 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)" } } } 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) { # StandardAttributes block is optional for some types (e.g. Enum) Report-OK "5. StandardAttributes: absent (optional 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} and optional PeriodAdjustment $isDynamic = ($mdType -eq "AccountingRegister" -and ($saName -match '^ExtDimension\d+$' -or $saName -match '^ExtDimensionType\d+$' -or $saName -eq "PeriodAdjustment")) # CalculationRegister has conditional period attrs $isCalcDynamic = ($mdType -eq "CalculationRegister" -and $saName -in @("ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod")) if (-not $isDynamic -and -not $isCalcDynamic) { 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" } } } 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" -and $kind -ne "Column") 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" } } if ($script:stopped) { & $finalize; exit 1 } # --- Check 7b: Reserved attribute names --- $reservedAttrNames = @( "Ref","DeletionMark","Code","Description","Date","Number","Posted","Parent","Owner", "IsFolder","Predefined","PredefinedDataName","Recorder","Period","LineNumber","Active", "Order","Type","OffBalance","Started","Completed","HeadTask","Executed","RoutePoint", "BusinessProcess","ThisNode","SentNo","ReceivedNo","CalculationType","RegistrationPeriod", "ReversingEntry","Account","ValueType","ActionPeriodIsBasic" ) if ($childObjNode) { $check7bOk = $true $attrNodes = $childObjNode.SelectNodes("md:Attribute", $ns) foreach ($attrNode in $attrNodes) { $attrProps = $attrNode.SelectSingleNode("md:Properties", $ns) if ($attrProps) { $attrNameNode = $attrProps.SelectSingleNode("md:Name", $ns) if ($attrNameNode -and $attrNameNode.InnerText) { $an = $attrNameNode.InnerText if ($reservedAttrNames -contains $an) { Report-Warn "7b. Attribute '$an' conflicts with a standard attribute name" $check7bOk = $false } } } } if ($check7bOk) { Report-OK "7b. Reserved attribute names: no conflicts" } } 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" } } 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" } } 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++ } } # AccountingRegister: ChartOfAccounts must not be empty if ($mdType -eq "AccountingRegister") { $coa = $propsNode.SelectSingleNode("md:ChartOfAccounts", $ns) if (-not $coa -or -not $coa.InnerText.Trim()) { Report-Error "10. AccountingRegister: empty ChartOfAccounts" $check10Ok = $false $check10Issues++ Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"ChartOfAccounts=ChartOfAccounts.XXX`"" } } # CalculationRegister: ChartOfCalculationTypes must not be empty if ($mdType -eq "CalculationRegister") { $coct = $propsNode.SelectSingleNode("md:ChartOfCalculationTypes", $ns) if (-not $coct -or -not $coct.InnerText.Trim()) { Report-Error "10. CalculationRegister: empty ChartOfCalculationTypes" $check10Ok = $false $check10Issues++ Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"ChartOfCalculationTypes=ChartOfCalculationTypes.XXX`"" } } # BusinessProcess: Task should not be empty if ($mdType -eq "BusinessProcess") { $taskProp = $propsNode.SelectSingleNode("md:Task", $ns) if (-not $taskProp -or -not $taskProp.InnerText.Trim()) { Report-Warn "10. BusinessProcess: empty Task reference" $check10Issues++ Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"Task=Task.XXX`"" } } # CalculationRegister: ActionPeriod=true requires non-empty Schedule if ($mdType -eq "CalculationRegister") { $actionPeriod = $propsNode.SelectSingleNode("md:ActionPeriod", $ns) if ($actionPeriod -and $actionPeriod.InnerText -eq "true") { $schedule = $propsNode.SelectSingleNode("md:Schedule", $ns) if (-not $schedule -or -not $schedule.InnerText.Trim()) { Report-Warn "10. CalculationRegister: ActionPeriod=true but Schedule is empty — platform requires a schedule register" $check10Issues++ } } } # DocumentJournal: RegisteredDocuments should not be empty if ($mdType -eq "DocumentJournal") { $regDocs = $propsNode.SelectSingleNode("md:RegisteredDocuments", $ns) $hasRegDocs = $false if ($regDocs) { $items = $regDocs.SelectNodes("v8:Type", $ns) if ($items.Count -gt 0) { $hasRegDocs = $true } } if (-not $hasRegDocs) { Report-Warn "10. DocumentJournal: no RegisteredDocuments specified" $check10Issues++ } } # ChartOfAccounts: ExtDimensionTypes should be set if MaxExtDimensionCount > 0 if ($mdType -eq "ChartOfAccounts") { $maxExtDim = $propsNode.SelectSingleNode("md:MaxExtDimensionCount", $ns) if ($maxExtDim -and [int]$maxExtDim.InnerText -gt 0) { $edt = $propsNode.SelectSingleNode("md:ExtDimensionTypes", $ns) if (-not $edt -or -not $edt.InnerText.Trim()) { Report-Warn "10. ChartOfAccounts: MaxExtDimensionCount>0 but ExtDimensionTypes is empty" $check10Issues++ Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"ExtDimensionTypes=ChartOfCharacteristicTypes.XXX`"" } } } # Register: must have at least one Dimension or Resource (platform rejects empty registers) $regTypesAll = @("AccumulationRegister","AccountingRegister","CalculationRegister","InformationRegister") if ($regTypesAll -contains $mdType -and $childObjNode) { $dims = $childObjNode.SelectNodes("md:Dimension", $ns).Count $ress = $childObjNode.SelectNodes("md:Resource", $ns).Count $attrs = $childObjNode.SelectNodes("md:Attribute", $ns).Count if (($dims + $ress + $attrs) -eq 0) { Report-Warn "10. $mdType`: no Dimensions, Resources, or Attributes — platform will reject" $check10Issues++ } } # Document: RegisterRecords references should point to existing objects in config if ($mdType -eq "Document" -and $script:configDir) { $regRecords = $propsNode.SelectSingleNode("md:RegisterRecords", $ns) if ($regRecords) { $items = $regRecords.SelectNodes("xr:Item", $ns) foreach ($item in $items) { $refVal = $item.InnerText.Trim() if (-not $refVal) { continue } # Parse "AccumulationRegister.Name" → dir AccumulationRegisters/Name $parts = $refVal -split '\.',2 if ($parts.Count -eq 2) { $refType = $parts[0]; $refName = $parts[1] $dirMap = @{ "AccumulationRegister"="AccumulationRegisters"; "InformationRegister"="InformationRegisters" "AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters" } $refDir = $dirMap[$refType] if ($refDir) { $refPath = Join-Path $script:configDir "$refDir/$refName" $refXml = Join-Path $script:configDir "$refDir/$refName.xml" if (-not (Test-Path $refPath) -and -not (Test-Path $refXml)) { Report-Warn "10. Document.RegisterRecords references '$refVal' but object not found in config" $check10Issues++ } } } } } } # Register: must have at least one registrar document $registerTypes = @("AccumulationRegister","AccountingRegister","CalculationRegister","InformationRegister") if ($registerTypes -contains $mdType -and $script:configDir -and $objName -ne "(unknown)") { $needsRegistrar = $true # InformationRegister with WriteMode=Independent does not need a registrar if ($mdType -eq "InformationRegister") { $writeMode = $propsNode.SelectSingleNode("md:WriteMode", $ns) if (-not $writeMode -or $writeMode.InnerText -ne "RecorderSubordinate") { $needsRegistrar = $false } } if ($needsRegistrar) { $regRef = "$mdType.$objName" $docsDir = Join-Path $script:configDir "Documents" $hasRegistrar = $false if (Test-Path $docsDir) { $docXmls = Get-ChildItem $docsDir -Filter "*.xml" -File -ErrorAction SilentlyContinue foreach ($docXml in $docXmls) { $content = [System.IO.File]::ReadAllText($docXml.FullName, [System.Text.Encoding]::UTF8) if ($content.Contains($regRef)) { $hasRegistrar = $true break } } } if (-not $hasRegistrar) { Report-Warn "10. $mdType`: no registrar document found (none references '$regRef' in RegisterRecords)" $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)" } } if ($script:stopped) { & $finalize; exit 1 } # --- Check 12: Forbidden properties per type --- if ($propsNode -and $forbiddenProperties.ContainsKey($mdType)) { $forbidden = $forbiddenProperties[$mdType] $check12Ok = $true foreach ($fp in $forbidden) { $fpNode = $propsNode.SelectSingleNode("md:$fp", $ns) if ($fpNode) { Report-Error "12. Forbidden property '$fp' present in $mdType (will fail on LoadConfigFromFiles)" $check12Ok = $false } } if ($check12Ok) { Report-OK "12. Forbidden properties: none found" } } if ($script:stopped) { & $finalize; exit 1 } # --- Check 13: Method reference validation (EventSubscription.Handler, ScheduledJob.MethodName) --- if ($propsNode -and $mdType -in @("EventSubscription","ScheduledJob") -and $script:configDir) { $check13Ok = $true $methodRef = $null $propLabel = $null if ($mdType -eq "EventSubscription") { $hNode = $propsNode.SelectSingleNode("md:Handler", $ns) if ($hNode) { $methodRef = $hNode.InnerText.Trim() } $propLabel = "Handler" } elseif ($mdType -eq "ScheduledJob") { $mNode = $propsNode.SelectSingleNode("md:MethodName", $ns) if ($mNode) { $methodRef = $mNode.InnerText.Trim() } $propLabel = "MethodName" } if ($methodRef) { $parts = $methodRef.Split('.') # Format: CommonModule.ModuleName.ProcedureName (3 parts) or ModuleName.ProcedureName (2 parts, legacy) if ($parts.Count -eq 3 -and $parts[0] -eq "CommonModule") { $cmName = $parts[1] $procName = $parts[2] } elseif ($parts.Count -eq 2) { $cmName = $parts[0] $procName = $parts[1] } else { Report-Error "13. ${mdType}.${propLabel} = '$methodRef': expected format 'CommonModule.ModuleName.ProcedureName'" $check13Ok = $false $cmName = $null $procName = $null } if ($cmName) { $cmXml = Join-Path (Join-Path $script:configDir "CommonModules") "$cmName.xml" if (-not (Test-Path $cmXml)) { Report-Error "13. ${mdType}.${propLabel}: CommonModule '$cmName' not found (expected $cmXml)" $check13Ok = $false } else { # Check BSL file for exported procedure $bslPath = Join-Path (Join-Path (Join-Path $script:configDir "CommonModules") $cmName) "Ext/Module.bsl" if (Test-Path $bslPath) { $bslContent = [System.IO.File]::ReadAllText($bslPath, [System.Text.Encoding]::UTF8) # Match: Procedure/Function ProcName(...) Export or Процедура/Функция ProcName(...) Экспорт $exportPattern = "(?mi)^[\s]*(Procedure|Function|Процедура|Функция)\s+$([regex]::Escape($procName))\s*\(.*\)\s+(Export|Экспорт)" if (-not [regex]::IsMatch($bslContent, $exportPattern)) { Report-Warn "13. ${mdType}.${propLabel}: procedure '$procName' not found as exported in CommonModule '$cmName'" $check13Ok = $false } } else { Report-Warn "13. ${mdType}.${propLabel}: BSL file not found ($bslPath), cannot verify procedure" } } } } if ($check13Ok) { Report-OK "13. Method reference: $propLabel = '$methodRef'" } } if ($script:stopped) { & $finalize; exit 1 } # --- Check 14: DocumentJournal Column content --- if ($mdType -eq "DocumentJournal" -and $childObjNode) { $columns = $childObjNode.SelectNodes("md:Column", $ns) $check14Ok = $true $colCount = 0 $emptyRefCount = 0 foreach ($col in $columns) { $colCount++ $colProps = $col.SelectSingleNode("md:Properties", $ns) $colNameNode = if ($colProps) { $colProps.SelectSingleNode("md:Name", $ns) } else { $null } $colName = if ($colNameNode) { $colNameNode.InnerText } else { "(unnamed)" } $refs = if ($colProps) { $colProps.SelectSingleNode("md:References", $ns) } else { $null } $hasItems = $false if ($refs) { $items = $refs.SelectNodes("xr:Item", $ns) if ($items.Count -gt 0) { $hasItems = $true } } if (-not $hasItems) { Report-Error "14. DocumentJournal Column '$colName': empty References (will fail on LoadConfigFromFiles)" $check14Ok = $false $emptyRefCount++ } } if ($check14Ok -and $colCount -gt 0) { Report-OK "14. DocumentJournal Columns: $colCount column(s), all have References" } elseif ($colCount -eq 0) { Report-OK "14. DocumentJournal Columns: none" } } # --- Final output --- & $finalize if ($script:errors -gt 0) { exit 1 } exit 0