Files
cc-1c-skills/.claude/skills/meta-validate/scripts/meta-validate.ps1
T
Nick ShirokovandClaude Opus 4.8 fbdf07e18a fix(meta-edit): modify-property Type — структурный дескриптор типа, guard от порчи
Корневой modify-property Type у ПВХ/Константы расплющивал структурный <Type>
(<v8:Type> + квалификаторы) в скалярный текст, а meta-validate это пропускал.

meta-edit (v1.21): modify-property Type перестраивает дескриптор через готовый
build_value_type_xml (составной тип, квалификаторы, ref-типы); прочие структурные
свойства с дочерними узлами → ошибка до записи файла вместо тихой порчи.

meta-validate (v1.10): корневой <Type> со скалярным текстом без <v8:Type>/<v8:TypeSet>
теперь ошибка (был false negative).

Порты PS1/PY синхронны. Регрессионные кейсы: modify-property-type-pvh (структурный
Type + ref), error-scalar-root-type (детект порчи).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 15:47:59 +03:00

1504 lines
56 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# meta-validate v1.10 — Validate 1C metadata object structure (+корневой <Type>: скаляр без структуры = ошибка)
# 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 — прощающий ввод для плоских объектов (SessionParameter, CommonAttribute,
# DefinedType, WSReference, … — один .xml без папки): дописать .xml к голому имени
if (-not (Test-Path $ObjectPath) -and -not [System.IO.Path]::HasExtension($ObjectPath)) {
if (Test-Path "$ObjectPath.xml") { $ObjectPath = "$ObjectPath.xml" }
}
# 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"
)
# Валидные типы метаданных без глубоких правил валидации — раньше падали как "Unrecognized"
# (ложная ошибка на валидном объекте). Для них выполняется базовая структурная проверка (root/uuid/Name).
$structuralOnlyTypes = @(
"Subsystem","Role","CommonForm","CommonCommand","CommandGroup","CommonAttribute",
"CommonTemplate","CommonPicture","SessionParameter","SettingsStorage","FilterCriterion",
"FunctionalOption","FunctionalOptionsParameter","Language","Style","StyleItem",
"WSReference","XDTOPackage","DocumentNumerator","Sequence"
)
# 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" = @()
}
# Группы командного интерфейса (зеркало meta-compile): раздела — без commandParameterType; формы — с параметром.
$sectionCommandGroups = @("NavigationPanelImportant","NavigationPanelOrdinary","NavigationPanelSeeAlso","ActionsPanelCreate","ActionsPanelReports","ActionsPanelTools")
$formCommandGroups = @("FormCommandBarImportant","FormCommandBarCreateBasedOn","FormNavigationPanelImportant","FormNavigationPanelGoTo","FormNavigationPanelSeeAlso")
$validCommandGroups = $sectionCommandGroups + $formCommandGroups
# 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","HierarchyOfItems")
"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) -and ($structuralOnlyTypes -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"
}
# --- Structural-only types: базовая проверка (Name), без type-specific правил ---
if ($structuralOnlyTypes -contains $mdType) {
if ($objName -eq "(unknown)") {
Report-Error "3. Properties: missing or empty Name"
} elseif ($objName -notmatch $identPattern) {
Report-Error "3. Properties: Name '$objName' is not a valid 1C identifier"
} else {
Report-OK "3. Properties: Name=`"$objName`" (базовая структурная проверка для $mdType)"
}
& $finalize
if ($script:errors -gt 0) { exit 1 }
exit 0
}
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++
}
}
# Корневой <Type> (дескриптор типа значения — Константа, ПВХ) должен быть структурным:
# <v8:Type>/<v8:TypeSet>, а не скалярный текст. Скаляр = повреждённый тип (напр. после
# старого meta-edit modify-property Type). См. issue #42.
$rootTypeEl = $propsNode.SelectSingleNode("md:Type", $ns)
if ($rootTypeEl) {
$v8Types = $rootTypeEl.SelectNodes("v8:Type", $ns)
$v8TypeSets = $rootTypeEl.SelectNodes("v8:TypeSet", $ns)
$scalarText = ""
foreach ($cn in $rootTypeEl.ChildNodes) {
if ($cn.NodeType -eq 'Text' -or $cn.NodeType -eq 'CDATA') {
$t = $cn.Value.Trim()
if ($t) { $scalarText = $t; break }
}
}
if ($v8Types.Count -eq 0 -and $v8TypeSets.Count -eq 0 -and $scalarText) {
Report-Error "4. Property <Type> содержит скалярный текст '$scalarText' без структуры типа (<v8:Type>/<v8:TypeSet>) — повреждённый дескриптор типа значения"
$check4Ok = $false
}
}
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 (типозависимо: стандартные реквизиты ДАННОГО типа, EN+RU) ---
# Совпадение имени собственного реквизита со стандартным (англ. или рус.) платформа не примет → ошибка.
$reservedEnRu = @{
"Ref"="Ссылка"; "DeletionMark"="ПометкаУдаления"; "Code"="Код"; "Description"="Наименование"
"Date"="Дата"; "Number"="Номер"; "Posted"="Проведен"; "Parent"="Родитель"; "Owner"="Владелец"
"IsFolder"="ЭтоГруппа"; "Predefined"="Предопределенный"; "PredefinedDataName"="ИмяПредопределенныхДанных"
"Recorder"="Регистратор"; "Period"="Период"; "LineNumber"="НомерСтроки"; "Active"="Активность"
"Order"="Порядок"; "Type"="Тип"; "OffBalance"="Забалансовый"; "RecordType"="ВидДвижения"
"Started"="Стартован"; "Completed"="Завершен"; "HeadTask"="ВедущаяЗадача"
"Executed"="Выполнена"; "RoutePoint"="ТочкаМаршрута"; "BusinessProcess"="БизнесПроцесс"
"ThisNode"="ЭтотУзел"; "SentNo"="НомерОтправленного"; "ReceivedNo"="НомерПринятого"
"CalculationType"="ВидРасчета"; "RegistrationPeriod"="ПериодРегистрации"; "ReversingEntry"="СторноЗапись"
"Account"="Счет"; "ValueType"="ТипЗначения"; "ActionPeriodIsBasic"="ПериодДействияБазовый"
}
$stdForType = $standardAttributesByType[$mdType]
if ($childObjNode -and $stdForType) {
# Множество зарезервированных имён (EN + RU) в нижнем регистре.
$reservedSet = @{}
foreach ($en in $stdForType) {
$reservedSet[$en.ToLower()] = $true
$ru = $reservedEnRu[$en]
if ($ru) { $reservedSet[$ru.ToLower()] = $true }
}
$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 ($reservedSet.ContainsKey($an.ToLower())) {
Report-Error "7b. Attribute '$an' conflicts with a standard attribute of $mdType"
$check7bOk = $false
}
}
}
}
if ($check7bOk) {
Report-OK "7b. Reserved attribute names: no conflicts"
}
} elseif ($childObjNode) {
Report-OK "7b. Reserved attribute names: no conflicts (no standard set for $mdType)"
}
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"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 15: Commands — Group обязателен/валиден; секц.группа несовместима с CommandParameterType ---
if ($childObjNode) {
$commands = $childObjNode.SelectNodes("md:Command", $ns)
$check15Ok = $true
$cmdCount = 0
foreach ($cmd in $commands) {
if ($script:stopped) { break }
$cmdCount++
$cuuid = $cmd.GetAttribute("uuid")
$cmdProps = $cmd.SelectSingleNode("md:Properties", $ns)
$cmdNameNode = if ($cmdProps) { $cmdProps.SelectSingleNode("md:Name", $ns) } else { $null }
$cmdName = if ($cmdNameNode -and $cmdNameNode.InnerText) { $cmdNameNode.InnerText } else { "(unnamed)" }
if (-not $cuuid -or $cuuid -notmatch $guidPattern) {
Report-Error "15. Command '$cmdName': missing or invalid uuid"; $check15Ok = $false
}
if ($cmdName -eq "(unnamed)") {
Report-Error "15. Command (uuid=$cuuid): missing or empty Name"; $check15Ok = $false
}
$groupNode = if ($cmdProps) { $cmdProps.SelectSingleNode("md:Group", $ns) } else { $null }
$groupVal = if ($groupNode) { $groupNode.InnerText.Trim() } else { "" }
if (-not $groupVal) {
Report-Error "15. Command '$cmdName': не задана группа (Group) — 1С отвергает при загрузке"; $check15Ok = $false
} elseif (($validCommandGroups -notcontains $groupVal) -and ($groupVal -notmatch '^CommandGroup\.')) {
Report-Error "15. Command '$cmdName': неизвестная группа '$groupVal'. Валидные: $($validCommandGroups -join ', '); либо CommandGroup.<Имя>"; $check15Ok = $false
} elseif ($sectionCommandGroups -contains $groupVal) {
$cptNode = if ($cmdProps) { $cmdProps.SelectSingleNode("md:CommandParameterType", $ns) } else { $null }
$hasCpt = $cptNode -and (($cptNode.SelectNodes("v8:Type", $ns).Count -gt 0) -or ($cptNode.SelectNodes("v8:TypeSet", $ns).Count -gt 0))
if ($hasCpt) {
Report-Error "15. Command '$cmdName': тип параметра (CommandParameterType) недоступен для команд командного интерфейса раздела ('$groupVal')"; $check15Ok = $false
}
}
}
if ($check15Ok -and $cmdCount -gt 0) {
Report-OK "15. Commands: $cmdCount command(s), groups valid"
}
}
# --- Check 16: Reference type existence — типы вида CatalogRef.X должны разрешаться в объекты конфигурации ---
# WARN-уровень: ложное срабатывание на частичных выгрузках хуже пропуска. Расширения (CFE) пропускаем —
# их типы ссылаются на объекты базовой конфигурации, которых нет в выгрузке расширения.
if ($script:configDir) {
$isExtension = $false
$cfgXmlPath = Join-Path $script:configDir "Configuration.xml"
if (Test-Path $cfgXmlPath) {
$cfgContent = [System.IO.File]::ReadAllText($cfgXmlPath, [System.Text.Encoding]::UTF8)
if ($cfgContent.Contains("ConfigurationExtensionPurpose")) { $isExtension = $true }
}
if (-not $isExtension) {
$refDirMap = @{
"CatalogRef"="Catalogs"; "DocumentRef"="Documents"; "EnumRef"="Enums"
"ChartOfAccountsRef"="ChartsOfAccounts"; "ChartOfCharacteristicTypesRef"="ChartsOfCharacteristicTypes"
"ChartOfCalculationTypesRef"="ChartsOfCalculationTypes"; "BusinessProcessRef"="BusinessProcesses"
"ExchangePlanRef"="ExchangePlans"; "TaskRef"="Tasks"; "DefinedType"="DefinedTypes"
}
$typeNodes = $xmlDoc.SelectNodes("//v8:Type", $ns)
$checkedRefs = @{} # refKey -> $true если найден; для OK-условия
$missingRefs = @{} # refKey -> refDir
foreach ($tn in $typeNodes) {
$tv = $tn.InnerText.Trim()
if (-not $tv) { continue }
$colonIdx = $tv.IndexOf(':')
if ($colonIdx -ge 0) { $tv = $tv.Substring($colonIdx + 1) }
$dotIdx = $tv.IndexOf('.')
if ($dotIdx -lt 0) { continue }
$refCat = $tv.Substring(0, $dotIdx)
$refName = $tv.Substring($dotIdx + 1)
$refDir = $refDirMap[$refCat]
if (-not $refDir -or -not $refName) { continue }
$refKey = "$refCat.$refName"
if ($checkedRefs.ContainsKey($refKey)) { continue }
$refFolder = Join-Path $script:configDir (Join-Path $refDir $refName)
$refFile = Join-Path $script:configDir (Join-Path $refDir "$refName.xml")
if ((Test-Path $refFolder) -or (Test-Path $refFile)) {
$checkedRefs[$refKey] = $true
} else {
$checkedRefs[$refKey] = $false
$missingRefs[$refKey] = $refDir
}
}
if ($missingRefs.Count -gt 0) {
foreach ($mk in ($missingRefs.Keys | Sort-Object)) {
Report-Warn "16. Ссылочный тип '$mk' не найден в конфигурации ($($missingRefs[$mk])/) — при загрузке будет ошибка неизвестного типа"
}
} elseif ($checkedRefs.Count -gt 0) {
Report-OK "16. Reference types: $($checkedRefs.Count) resolved"
}
}
}
# --- Final output ---
& $finalize
if ($script:errors -gt 0) {
exit 1
}
exit 0