Files
2026-06-04 09:28:00 +00:00

1341 lines
48 KiB
PowerShell

# 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