Files
cc-1c-skills/.kiro/skills/cfe-validate/scripts/cfe-validate.ps1
T

1152 lines
44 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.
# cfe-validate v1.8 — Validate 1C configuration extension structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ExtensionPath,
[switch]$Detailed,
[int]$MaxErrors = 30,
[string]$OutFile,
# Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией,
# пропускаются (о чём сказано в отчёте), остальные работают как раньше.
[string]$ConfigPath
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve path ---
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
}
if (Test-Path $ExtensionPath -PathType Container) {
$candidate = Join-Path $ExtensionPath "Configuration.xml"
if (Test-Path $candidate) {
$ExtensionPath = $candidate
} else {
Write-Host "[ERROR] No Configuration.xml found in directory: $ExtensionPath"
exit 1
}
}
if (-not (Test-Path $ExtensionPath)) {
Write-Host "[ERROR] File not found: $ExtensionPath"
exit 1
}
$resolvedPath = (Resolve-Path $ExtensionPath).Path
$configDir = Split-Path $resolvedPath -Parent
# --- 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: Extension.$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_]*$'
# 7 fixed ClassIds for Configuration
$validClassIds = @(
"9cd510cd-abfc-11d4-9434-004095e12fc7",
"9fcd25a0-4822-11d4-9414-008048da11f9",
"e3687481-0a87-462c-a166-9f34594f9bba",
"9de14907-ec23-4a07-96f0-85521cb6b53b",
"51f2d5d8-ea4d-4064-8892-82951750031e",
"e68182ea-4237-4383-967f-90c1e3370bc7",
"fb282519-d103-4dd3-bc12-cb271d631dfc"
)
# 44 types in canonical order
$childObjectTypes = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
"Constant","CommonForm","Catalog","Document",
"DocumentNumerator","Sequence","DocumentJournal","Enum",
"Report","DataProcessor","InformationRegister","AccumulationRegister",
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
"ChartOfCalculationTypes","CalculationRegister",
"BusinessProcess","Task","IntegrationService"
)
# Type -> directory mapping
$childTypeDirMap = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
"SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"
"CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"; "Constant"="Constants"
"CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents"
"DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences"
"DocumentJournal"="DocumentJournals"; "Enum"="Enums"; "Report"="Reports"
"DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters"
"AccumulationRegister"="AccumulationRegisters"
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
"CalculationRegister"="CalculationRegisters"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
"IntegrationService"="IntegrationServices"
}
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
# Неполный набор в заимствованной оболочке платформа отвергает при загрузке: «отсутствует один
# или более типов объекта <Тип>». Типы, у которых GeneratedType нет вовсе (общие модули,
# подписки, регламентные задания и т.п.), в карте отсутствуют — для них проверка не выполняется.
$generatedTypeCategories = @{
"Catalog" = @("Object","Ref","Selection","List","Manager")
"Document" = @("Object","Ref","Selection","List","Manager")
"Enum" = @("Ref","Manager","List")
"Constant" = @("Manager","ValueManager","ValueKey")
"Report" = @("Object","Manager")
"DataProcessor" = @("Object","Manager")
"ExchangePlan" = @("Object","Ref","Selection","List","Manager")
"Task" = @("Object","Ref","Selection","List","Manager")
"BusinessProcess" = @("Object","Ref","Selection","List","Manager","RoutePointRef")
"ChartOfCharacteristicTypes" = @("Object","Ref","Selection","List","Manager","Characteristic")
"ChartOfAccounts" = @("Object","Ref","Selection","List","Manager","ExtDimensionTypes","ExtDimensionTypesRow")
"ChartOfCalculationTypes" = @("Object","Ref","Selection","List","Manager","DisplacingCalculationTypes","DisplacingCalculationTypesRow","BaseCalculationTypes","BaseCalculationTypesRow","LeadingCalculationTypes","LeadingCalculationTypesRow")
"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")
"DocumentJournal" = @("Selection","List","Manager")
"Sequence" = @("Record","Manager","RecordSet")
"FilterCriterion" = @("Manager","List")
"SettingsStorage" = @("Manager")
"IntegrationService" = @("Manager")
"WSReference" = @("Manager")
"DefinedType" = @("DefinedType")
}
# Стандартные реквизиты объектов: в ChildObjects их нет, но пути Объект.<Стандартный> законны.
# Имена зависят от варианта встроенного языка, поэтому держим оба написания.
$script:standardObjectFields = @(
"Code","Description","Ref","Parent","Owner","DeletionMark","Predefined","IsFolder","LineNumber",
"Number","Date","Posted","PredefinedDataName","RegisterRecords","DataVersion","RowsCount",
"Код","Наименование","Ссылка","Родитель","Владелец","ПометкаУдаления","Предопределенный",
"ЭтоГруппа","НомерСтроки","Номер","Дата","Проведен","ИмяПредопределенныхДанных",
"Движения","ВерсияДанных","КоличествоСтрок"
)
# Valid enum values for extension properties
$validEnumValues = @{
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
"ScriptVariant" = @("Russian","English")
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
}
# --- 1. Parse XML ---
Out-Line ""
$xmlDoc = $null
try {
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $false
$xmlDoc.Load($resolvedPath)
} catch {
Out-Line "=== Validation: Extension (parse failed) ==="
Out-Line ""
Report-Error "1. XML parse failed: $($_.Exception.Message)"
& $finalize
exit 1
}
# --- 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("app", "http://v8.1c.ru/8.2/managed-application/core")
$root = $xmlDoc.DocumentElement
# --- Check 1: Root structure ---
$check1Ok = $true
$expectedNs = "http://v8.1c.ru/8.3/MDClasses"
if ($root.LocalName -ne "MetaDataObject") {
Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'"
& $finalize
exit 1
}
if ($root.NamespaceURI -ne $expectedNs) {
Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'"
$check1Ok = $false
}
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
}
# Must have Configuration child
$cfgNode = $null
foreach ($child in $root.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration" -and $child.NamespaceURI -eq $expectedNs) {
$cfgNode = $child; break
}
}
if (-not $cfgNode) {
Report-Error "1. No <Configuration> element found inside MetaDataObject"
& $finalize
exit 1
}
# UUID
$cfgUuid = $cfgNode.GetAttribute("uuid")
if (-not $cfgUuid) {
Report-Error "1. Missing uuid on <Configuration>"
$check1Ok = $false
} elseif ($cfgUuid -notmatch $guidPattern) {
Report-Error "1. Invalid uuid '$cfgUuid' on <Configuration>"
$check1Ok = $false
}
# Get name early for header
$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns)
$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null }
$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" }
$script:output.Insert(0, "=== Validation: Extension.$objName ===$([Environment]::NewLine)") | Out-Null
if ($check1Ok) {
Report-OK "1. Root structure: MetaDataObject/Configuration, version $version"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 2: InternalInfo ---
$internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns)
$check2Ok = $true
if (-not $internalInfo) {
Report-Error "2. InternalInfo: missing"
} else {
$contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns)
if ($contained.Count -ne 7) {
Report-Warn "2. InternalInfo: expected 7 ContainedObject, found $($contained.Count)"
}
$foundClassIds = @{}
foreach ($co in $contained) {
$classId = $co.SelectSingleNode("xr:ClassId", $ns)
$objectId = $co.SelectSingleNode("xr:ObjectId", $ns)
if (-not $classId -or -not $classId.InnerText) {
Report-Error "2. ContainedObject missing ClassId"
$check2Ok = $false
continue
}
$cid = $classId.InnerText
if ($validClassIds -notcontains $cid) {
Report-Error "2. Unknown ClassId: $cid"
$check2Ok = $false
}
if ($foundClassIds.ContainsKey($cid)) {
Report-Error "2. Duplicate ClassId: $cid"
$check2Ok = $false
}
$foundClassIds[$cid] = $true
if (-not $objectId -or -not $objectId.InnerText) {
Report-Error "2. ContainedObject missing ObjectId for ClassId $cid"
$check2Ok = $false
} elseif ($objectId.InnerText -notmatch $guidPattern) {
Report-Error "2. Invalid ObjectId '$($objectId.InnerText)' for ClassId $cid"
$check2Ok = $false
}
}
$missingIds = @($validClassIds | Where-Object { -not $foundClassIds.ContainsKey($_) })
if ($missingIds.Count -gt 0) {
Report-Warn "2. Missing ClassIds: $($missingIds.Count) of 7"
}
if ($check2Ok) {
Report-OK "2. InternalInfo: $($contained.Count) ContainedObject, all ClassIds valid"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 3: Extension-specific properties ---
if (-not $propsNode) {
Report-Error "3. Properties block missing"
} else {
$check3Ok = $true
# ObjectBelonging = Adopted
$obNode = $propsNode.SelectSingleNode("md:ObjectBelonging", $ns)
if (-not $obNode -or $obNode.InnerText -ne "Adopted") {
Report-Error "3. ObjectBelonging must be 'Adopted', got '$($obNode.InnerText)'"
$check3Ok = $false
}
# Name
if (-not $nameNode -or -not $nameNode.InnerText) {
Report-Error "3. Name is missing or empty"
$check3Ok = $false
} else {
$nameVal = $nameNode.InnerText
if ($nameVal -notmatch $identPattern) {
Report-Error "3. Name '$nameVal' is not a valid 1C identifier"
$check3Ok = $false
}
}
# ConfigurationExtensionPurpose
$purposeNode = $propsNode.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns)
$validPurposes = @("Patch","Customization","AddOn")
if (-not $purposeNode -or -not $purposeNode.InnerText) {
Report-Error "3. ConfigurationExtensionPurpose is missing"
$check3Ok = $false
} elseif ($validPurposes -notcontains $purposeNode.InnerText) {
Report-Error "3. ConfigurationExtensionPurpose '$($purposeNode.InnerText)' invalid (expected: Patch, Customization, AddOn)"
$check3Ok = $false
}
# NamePrefix
$prefixNode = $propsNode.SelectSingleNode("md:NamePrefix", $ns)
if (-not $prefixNode -or -not $prefixNode.InnerText) {
Report-Warn "3. NamePrefix is empty"
}
# KeepMappingToExtendedConfigurationObjectsByIDs
$keepMapNode = $propsNode.SelectSingleNode("md:KeepMappingToExtendedConfigurationObjectsByIDs", $ns)
if (-not $keepMapNode) {
Report-Warn "3. KeepMappingToExtendedConfigurationObjectsByIDs is missing"
}
# DefaultLanguage
$defLangNode = $propsNode.SelectSingleNode("md:DefaultLanguage", $ns)
$defLang = if ($defLangNode -and $defLangNode.InnerText) { $defLangNode.InnerText } else { "" }
if ($check3Ok) {
$purposeVal = if ($purposeNode) { $purposeNode.InnerText } else { "?" }
$prefixVal = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "(empty)" }
Report-OK "3. Extension properties: Name=`"$objName`", Purpose=$purposeVal, Prefix=$prefixVal"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 4: Enum property values ---
if ($propsNode) {
$enumChecked = 0
$check4Ok = $true
foreach ($propName in $validEnumValues.Keys) {
$propNode = $propsNode.SelectSingleNode("md:$propName", $ns)
if ($propNode -and $propNode.InnerText) {
$val = $propNode.InnerText
$allowed = $validEnumValues[$propName]
if ($allowed -notcontains $val) {
Report-Error "4. Property '$propName' has invalid value '$val'"
$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: ChildObjects — valid types, no duplicates, order ---
$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns)
if (-not $childObjNode) {
Report-Error "5. ChildObjects block missing"
} else {
$check5Ok = $true
$totalCount = 0
$script:childObjectIndex = @{}
$duplicates = @{}
$typeFirstIndex = @{}
$lastTypeOrder = -1
$orderOk = $true
foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
$typeName = $child.LocalName
$objNameVal = $child.InnerText
$typeIdx = $childObjectTypes.IndexOf($typeName)
if ($typeIdx -lt 0) {
Report-Error "5. Unknown type '$typeName' in ChildObjects"
$check5Ok = $false
} else {
if (-not $typeFirstIndex.ContainsKey($typeName)) {
$typeFirstIndex[$typeName] = $typeIdx
if ($typeIdx -lt $lastTypeOrder) {
Report-Warn "5. Type '$typeName' is out of canonical order (after type at position $lastTypeOrder)"
$orderOk = $false
}
$lastTypeOrder = $typeIdx
}
}
if (-not $script:childObjectIndex.ContainsKey($typeName)) { $script:childObjectIndex[$typeName] = @{} }
if ($script:childObjectIndex[$typeName].ContainsKey($objNameVal)) {
if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) {
Report-Error "5. Duplicate: $typeName.$objNameVal"
$duplicates["$typeName.$objNameVal"] = $true
$check5Ok = $false
}
} else {
$script:childObjectIndex[$typeName][$objNameVal] = $true
}
$totalCount++
}
$typeCount = $script:childObjectIndex.Count
if ($check5Ok) {
$orderInfo = if ($orderOk) { ", order correct" } else { "" }
Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 6: DefaultLanguage references existing Language in ChildObjects ---
if ($defLang -and $childObjNode) {
$langName = $defLang
if ($langName.StartsWith("Language.")) {
$langName = $langName.Substring(9)
}
$found = $false
foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language" -and $child.InnerText -eq $langName) {
$found = $true; break
}
}
if ($found) {
Report-OK "6. DefaultLanguage `"$defLang`" found in ChildObjects"
} else {
Report-Error "6. DefaultLanguage `"$defLang`" not found in ChildObjects"
}
} else {
if (-not $defLang) {
Report-Warn "6. Cannot check DefaultLanguage (empty)"
} else {
Report-Warn "6. Cannot check DefaultLanguage (no ChildObjects)"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 7: Language files exist ---
if ($childObjNode) {
$langNames = @()
foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language") {
$langNames += $child.InnerText
}
}
if ($langNames.Count -gt 0) {
$existCount = 0
foreach ($ln in $langNames) {
$langFile = Join-Path (Join-Path $configDir "Languages") "$ln.xml"
if (Test-Path $langFile) {
$existCount++
} else {
Report-Warn "7. Language file missing: Languages/$ln.xml"
}
}
if ($existCount -eq $langNames.Count) {
Report-OK "7. Language files: $existCount/$($langNames.Count) exist"
}
} else {
Report-Warn "7. No Language entries in ChildObjects"
}
} else {
Report-Warn "7. Cannot check language files (no ChildObjects)"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 8: Object directories exist ---
if ($childObjNode) {
$dirsToCheck = @{}
foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
$typeName = $child.LocalName
if ($typeName -eq "Language") { continue }
if ($childTypeDirMap.ContainsKey($typeName)) {
$dirName = $childTypeDirMap[$typeName]
if (-not $dirsToCheck.ContainsKey($dirName)) {
$dirsToCheck[$dirName] = 0
}
$dirsToCheck[$dirName] = $dirsToCheck[$dirName] + 1
}
}
$missingDirs = @()
foreach ($dir in $dirsToCheck.Keys) {
$dirPath = Join-Path $configDir $dir
if (-not (Test-Path $dirPath -PathType Container)) {
$missingDirs += "$dir ($($dirsToCheck[$dir]) objects)"
}
}
if ($missingDirs.Count -eq 0) {
Report-OK "8. Object directories: $($dirsToCheck.Count) directories, all exist"
} else {
foreach ($md in $missingDirs) {
Report-Warn "8. Missing directory: $md"
}
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 9: Borrowed objects validation + Check 10: Sub-items ---
$script:enumValuesIndex = @{}
$script:borrowedTSIndex = @{}
$script:formList = @()
# Helper: check if sub-item has explicit borrowed metadata
function Test-BorrowedSubItem {
param($subItem, $nsm)
$subProps = $subItem.SelectSingleNode("md:Properties", $nsm)
if (-not $subProps) { return $false }
$subOb = $subProps.SelectSingleNode("md:ObjectBelonging", $nsm)
if ($subOb -and $subOb.InnerText) { return $true }
$subExt = $subProps.SelectSingleNode("md:ExtendedConfigurationObject", $nsm)
return [bool]($subExt -and $subExt.InnerText)
}
# Helper: validate a borrowed Attribute/EnumValue sub-item
function Validate-BorrowedSubItem {
param([string]$checkNum, [string]$context, [string]$subType, $subItem, $nsm)
$subProps = $subItem.SelectSingleNode("md:Properties", $nsm)
if (-not $subProps) {
Report-Error "${checkNum}. ${context}: ${subType} missing Properties"
return $false
}
$ok = $true
$subOb = $subProps.SelectSingleNode("md:ObjectBelonging", $nsm)
if (-not $subOb -or $subOb.InnerText -ne "Adopted") {
Report-Error "${checkNum}. ${context}: ${subType} ObjectBelonging must be 'Adopted'"
$ok = $false
}
$subName = $subProps.SelectSingleNode("md:Name", $nsm)
if (-not $subName -or -not $subName.InnerText) {
Report-Error "${checkNum}. ${context}: ${subType} missing Name"
$ok = $false
}
$subExt = $subProps.SelectSingleNode("md:ExtendedConfigurationObject", $nsm)
if (-not $subExt -or -not $subExt.InnerText) {
Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) missing ExtendedConfigurationObject"
$ok = $false
} elseif ($subExt.InnerText -notmatch $guidPattern) {
Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) invalid ExtendedConfigurationObject"
$ok = $false
}
return $ok
}
if ($childObjNode) {
$borrowedCount = 0
$borrowedOk = 0
$check9Ok = $true
$check10Ok = $true
$subItemCount = 0
foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
$typeName = $child.LocalName
$childName = $child.InnerText
if ($typeName -eq "Language") { continue }
if (-not $childTypeDirMap.ContainsKey($typeName)) { continue }
$dirName = $childTypeDirMap[$typeName]
$objFile = Join-Path (Join-Path $configDir $dirName) "$childName.xml"
if (-not (Test-Path $objFile)) { continue }
# Parse object XML
$objDoc = $null
try {
$objDoc = New-Object System.Xml.XmlDocument
$objDoc.PreserveWhitespace = $false
$objDoc.Load($objFile)
} catch {
Report-Warn "9. Cannot parse $dirName/$childName.xml: $($_.Exception.Message)"
continue
}
$objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable)
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$objNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
# Find the object element (Catalog, Document, etc.)
$objRoot = $objDoc.DocumentElement
$objEl = $null
foreach ($c in $objRoot.ChildNodes) {
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
}
if (-not $objEl) { continue }
$objProps = $objEl.SelectSingleNode("md:Properties", $objNs)
if (-not $objProps) { continue }
# --- Check 9: ObjectBelonging + ExtendedConfigurationObject ---
$obNode = $objProps.SelectSingleNode("md:ObjectBelonging", $objNs)
if ($obNode -and $obNode.InnerText -eq "Adopted") {
$borrowedCount++
$extObj = $objProps.SelectSingleNode("md:ExtendedConfigurationObject", $objNs)
if (-not $extObj -or -not $extObj.InnerText) {
Report-Error "9. Borrowed ${typeName}.${childName}: missing ExtendedConfigurationObject"
$check9Ok = $false
} elseif ($extObj.InnerText -notmatch $guidPattern) {
Report-Error "9. Borrowed ${typeName}.${childName}: invalid ExtendedConfigurationObject UUID '$($extObj.InnerText)'"
$check9Ok = $false
} else {
$borrowedOk++
}
# Полнота набора GeneratedType: платформа отвергает оболочку с неполным набором
# («отсутствует один или более типов объекта ChartOfCharacteristicTypes»)
$expectedCats = $generatedTypeCategories[$typeName]
if ($expectedCats) {
$objInfo = $objEl.SelectSingleNode("md:InternalInfo", $objNs)
$foundCats = @{}
if ($objInfo) {
foreach ($gt in $objInfo.SelectNodes("xr:GeneratedType", $objNs)) {
$cat = $gt.GetAttribute("category")
if ($cat) { $foundCats[$cat] = $true }
}
}
$missingCats = @($expectedCats | Where-Object { -not $foundCats.ContainsKey($_) })
if ($missingCats.Count -gt 0) {
Report-Error "9. Borrowed ${typeName}.${childName}: missing GeneratedType categor$(if ($missingCats.Count -eq 1) { 'y' } else { 'ies' }) $($missingCats -join ', ')"
$check9Ok = $false
}
}
}
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
$objChildObjects = $objEl.SelectSingleNode("md:ChildObjects", $objNs)
if ($objChildObjects) {
$ctx = "${typeName}.${childName}"
foreach ($subItem in $objChildObjects.ChildNodes) {
if ($subItem.NodeType -ne 'Element') { continue }
$subType = $subItem.LocalName
if ($subType -eq "Attribute") {
if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue }
$subItemCount++
if (-not (Validate-BorrowedSubItem "10" $ctx "Attribute" $subItem $objNs)) {
$check10Ok = $false
}
}
elseif ($subType -eq "TabularSection") {
if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue }
$subItemCount++
if (-not (Validate-BorrowedSubItem "10" $ctx "TabularSection" $subItem $objNs)) {
$check10Ok = $false
} else {
# Check InternalInfo GeneratedTypes
$tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs)
$tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs)
$tsLabel = if ($tsName) { $tsName.InnerText } else { "?" }
# Индекс заимствованных ТЧ — по нему Check 12 сверяет <AdditionalColumns table="Объект.X">
if ($tsName) {
$tsKey = "${typeName}.${childName}"
if (-not $script:borrowedTSIndex.ContainsKey($tsKey)) { $script:borrowedTSIndex[$tsKey] = @{} }
$script:borrowedTSIndex[$tsKey][$tsName.InnerText] = $true
}
if (-not $tsInfo) {
Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo"
$check10Ok = $false
} else {
$gtNodes = $tsInfo.SelectNodes("xr:GeneratedType", $objNs)
$hasTSCat = $false; $hasTSRCat = $false
foreach ($gt in $gtNodes) {
$cat = $gt.GetAttribute("category")
if ($cat -eq "TabularSection") { $hasTSCat = $true }
if ($cat -eq "TabularSectionRow") { $hasTSRCat = $true }
}
if (-not $hasTSCat -or -not $hasTSRCat) {
Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing GeneratedType (need TabularSection + TabularSectionRow)"
$check10Ok = $false
}
}
# Recurse into TS ChildObjects/Attribute
$tsChildObjs = $subItem.SelectSingleNode("md:ChildObjects", $objNs)
if ($tsChildObjs) {
foreach ($tsAttr in $tsChildObjs.ChildNodes) {
if ($tsAttr.NodeType -ne 'Element' -or $tsAttr.LocalName -ne "Attribute") { continue }
if (-not (Test-BorrowedSubItem $tsAttr $objNs)) { continue }
$subItemCount++
if (-not (Validate-BorrowedSubItem "10" "${ctx}.ТЧ.${tsLabel}" "Attribute" $tsAttr $objNs)) {
$check10Ok = $false
}
}
}
}
}
elseif ($subType -eq "EnumValue" -and $typeName -eq "Enum") {
if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue }
$subItemCount++
if (Validate-BorrowedSubItem "10" $ctx "EnumValue" $subItem $objNs) {
$evName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs)
if ($evName -and $evName.InnerText) {
if (-not $script:enumValuesIndex.ContainsKey($childName)) {
$script:enumValuesIndex[$childName] = @{}
}
$script:enumValuesIndex[$childName][$evName.InnerText] = $true
}
} else {
$check10Ok = $false
}
}
elseif ($subType -eq "Form") {
$formName = $subItem.InnerText
if ($formName) {
$formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $configDir $dirName) $childName) "Forms") "${formName}.xml"
if (-not (Test-Path $formMetaFile)) {
Report-Error "10. ${ctx}: Form.${formName} metadata file missing"
$check10Ok = $false
}
$script:formList += @{
TypeName = $typeName; ObjName = $childName
FormName = $formName; DirName = $dirName
}
$subItemCount++
}
}
}
}
if ($script:stopped) { break }
}
if ($borrowedCount -eq 0) {
Report-OK "9. Borrowed objects: none found"
} elseif ($check9Ok) {
Report-OK "9. Borrowed objects: $borrowedOk/$borrowedCount validated"
}
if ($subItemCount -eq 0) {
Report-OK "10. Sub-items: none found"
} elseif ($check10Ok) {
Report-OK "10. Sub-items: $subItemCount validated (Attributes, TabularSections, EnumValues, Forms)"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 11: Borrowed form structure ---
$script:borrowedFormsWithTree = @()
$check11Ok = $true
$formCount = 0
foreach ($fi in $script:formList) {
$formCount++
$formBase = Join-Path (Join-Path (Join-Path (Join-Path $configDir $fi.DirName) $fi.ObjName) "Forms") $fi.FormName
$formMetaFile = Join-Path (Split-Path $formBase -Parent) "$($fi.FormName).xml"
$formXmlFile = Join-Path (Join-Path $formBase "Ext") "Form.xml"
$moduleBslFile = Join-Path (Join-Path (Join-Path $formBase "Ext") "Form") "Module.bsl"
$ctx = "$($fi.TypeName).$($fi.ObjName).Form.$($fi.FormName)"
# Validate form metadata XML
if (Test-Path $formMetaFile) {
try {
$fmDoc = New-Object System.Xml.XmlDocument
$fmDoc.PreserveWhitespace = $false
$fmDoc.Load($formMetaFile)
$fmNs = New-Object System.Xml.XmlNamespaceManager($fmDoc.NameTable)
$fmNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$fmEl = $null
foreach ($c in $fmDoc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $fmEl = $c; break }
}
if ($fmEl) {
$fmProps = $fmEl.SelectSingleNode("md:Properties", $fmNs)
if ($fmProps) {
$fmOb = $fmProps.SelectSingleNode("md:ObjectBelonging", $fmNs)
$isBorrowed = $fmOb -and $fmOb.InnerText -eq "Adopted"
if ($isBorrowed) {
$fmExt = $fmProps.SelectSingleNode("md:ExtendedConfigurationObject", $fmNs)
if (-not $fmExt -or $fmExt.InnerText -notmatch $guidPattern) {
Report-Error "11. ${ctx}: invalid/missing ExtendedConfigurationObject"
$check11Ok = $false
}
}
$fmType = $fmProps.SelectSingleNode("md:FormType", $fmNs)
if ($fmType -and $fmType.InnerText -ne "Managed") {
Report-Error "11. ${ctx}: FormType must be 'Managed', got '$($fmType.InnerText)'"
$check11Ok = $false
}
}
}
} catch {
Report-Warn "11. ${ctx}: Cannot parse metadata: $($_.Exception.Message)"
}
}
# Form.xml must exist
if (-not (Test-Path $formXmlFile)) {
Report-Error "11. ${ctx}: Ext/Form.xml missing"
$check11Ok = $false
continue
}
# Module.bsl should exist
if (-not (Test-Path $moduleBslFile)) {
Report-Warn "11. ${ctx}: Ext/Form/Module.bsl missing"
}
# Read Form.xml as raw text for BaseForm checks
$formRawText = [System.IO.File]::ReadAllText($formXmlFile, [System.Text.Encoding]::UTF8)
if ($formRawText -match '<BaseForm') {
# Check BaseForm has version
if ($formRawText -notmatch '<BaseForm[^>]+version=') {
Report-Warn "11. ${ctx}: <BaseForm> missing version attribute"
}
$script:borrowedFormsWithTree += @{
Path = $formXmlFile; RawText = $formRawText; Context = $ctx
}
}
}
if ($formCount -eq 0) {
Report-OK "11. Borrowed forms: none found"
} elseif ($check11Ok) {
$bfCount = $script:borrowedFormsWithTree.Count
Report-OK "11. Borrowed forms: $formCount validated ($bfCount with BaseForm)"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 12: Form dependency references ---
$platformStyleItems = @{
"TableHeaderBackColor"=$true; "AccentColor"=$true; "NormalTextFont"=$true
"FormBackColor"=$true; "ToolTipBackColor"=$true; "BorderColor"=$true
"FieldBackColor"=$true; "FieldTextColor"=$true; "ButtonBackColor"=$true
"ButtonTextColor"=$true; "AlternateRowColor"=$true; "SpecialTextColor"=$true
"TextFont"=$true; "ImportantColor"=$true; "FormTextColor"=$true
"SmallTextFont"=$true; "ExtraLargeTextFont"=$true; "LargeTextFont"=$true
"NormalTextColor"=$true; "GroupHeaderBackColor"=$true; "GroupHeaderFont"=$true
"ErrorColor"=$true; "SuccessColor"=$true; "WarningColor"=$true
}
$check12Ok = $true
$depCheckCount = 0
foreach ($bf in $script:borrowedFormsWithTree) {
$raw = $bf.RawText
$ctx = $bf.Context
$missingItems = @()
# CommonPicture references
$cpRefs = @{}
foreach ($m in [regex]::Matches($raw, '<xr:Ref>CommonPicture\.(\w+)</xr:Ref>')) {
$cpRefs[$m.Groups[1].Value] = $true
}
$cpIndex = $script:childObjectIndex["CommonPicture"]
foreach ($cpName in $cpRefs.Keys) {
$depCheckCount++
if (-not $cpIndex -or -not $cpIndex.ContainsKey($cpName)) {
$missingItems += "CommonPicture.${cpName}"
}
}
# StyleItem references
$siRefs = @{}
foreach ($m in [regex]::Matches($raw, 'style:([A-Za-z\u0410-\u044F\u0401\u0451_][A-Za-z0-9\u0410-\u044F\u0401\u0451_]*)')) {
$siRefs[$m.Groups[1].Value] = $true
}
$siIndex = $script:childObjectIndex["StyleItem"]
foreach ($siName in $siRefs.Keys) {
$depCheckCount++
if ($platformStyleItems.ContainsKey($siName)) { continue }
if (-not $siIndex -or -not $siIndex.ContainsKey($siName)) {
$missingItems += "StyleItem.${siName}"
}
}
# Enum DesignTimeRef references
$enumRefs = @{}
foreach ($m in [regex]::Matches($raw, 'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)')) {
$eKey = "$($m.Groups[1].Value).$($m.Groups[2].Value)"
$enumRefs[$eKey] = @{ Enum = $m.Groups[1].Value; Value = $m.Groups[2].Value }
}
$eIndex = $script:childObjectIndex["Enum"]
foreach ($entry in $enumRefs.Values) {
$depCheckCount++
if (-not $eIndex -or -not $eIndex.ContainsKey($entry.Enum)) {
$missingItems += "Enum.$($entry.Enum)"
} elseif (-not $script:enumValuesIndex.ContainsKey($entry.Enum) -or -not $script:enumValuesIndex[$entry.Enum].ContainsKey($entry.Value)) {
$missingItems += "Enum.$($entry.Enum).EnumValue.$($entry.Value)"
}
}
# <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в самой форме.
# Колонки есть, а самой ТЧ в расширении нет → платформа отвергает загрузку: «Неверный путь к
# данным» плюс «Колонки не могут быть добавлены к реквизиту».
$acTables = @{}
foreach ($m in [regex]::Matches($raw, '<AdditionalColumns table="Объект\.(\w+)"')) {
$acTables[$m.Groups[1].Value] = $true
}
# Соседние проверки этого блока эвристичны (имя стиля добывается регуляркой), поэтому там
# предупреждение. Здесь сигнал точный — имя ТЧ берётся из атрибута, — а последствие жёсткое,
# поэтому ошибка.
if ($acTables.Count -gt 0) {
$ownerKey = ($ctx -split '\.Form\.')[0]
$ownerTS = $script:borrowedTSIndex[$ownerKey]
foreach ($tblName in $acTables.Keys) {
$depCheckCount++
if (-not $ownerTS -or -not $ownerTS.ContainsKey($tblName)) {
Report-Error "12. ${ctx}: <AdditionalColumns table=`"Объект.${tblName}`"> — TabularSection.${tblName} not borrowed in extension"
$check12Ok = $false
}
}
}
foreach ($mi in $missingItems) {
Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension"
$check12Ok = $false
}
}
if ($script:borrowedFormsWithTree.Count -eq 0) {
Report-OK "12. Form dependencies: no borrowed forms with tree"
} elseif ($check12Ok) {
Report-OK "12. Form dependencies: $depCheckCount references checked"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 13: TypeLink with human-readable paths ---
$check13Ok = $true
$typeLinkCount = 0
foreach ($bf in $script:borrowedFormsWithTree) {
$raw = $bf.RawText
$ctx = $bf.Context
$matches = [regex]::Matches($raw, '<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>')
if ($matches.Count -gt 0) {
$typeLinkCount += $matches.Count
Report-Warn "13. ${ctx}: $($matches.Count) TypeLink(s) with human-readable Items.* DataPath (should be stripped)"
$check13Ok = $false
}
}
if ($script:borrowedFormsWithTree.Count -eq 0) {
Report-OK "13. TypeLink: no borrowed forms with tree"
} elseif ($check13Ok) {
Report-OK "13. TypeLink: clean"
}
# --- Check 14: пути Объект.* заимствованных форм против конфигурации-источника ---
# Требует -ConfigPath: отличить живой путь от висячего можно только по исходному объекту.
# «Объект.Партнер» валиден и без заимствования реквизита (наследуется от базы), а «Объект.Товары.Артикул»
# не разрешится нигде, если Артикул — не реквизит объекта и не колонка из <Columns> самой формы.
# Такой путь платформа отвергает на загрузке: «Неверный путь к данным».
if (-not $script:stopped -and $script:borrowedFormsWithTree.Count -gt 0) {
if (-not $ConfigPath) {
Out-Line "[INFO] 14. Пути Объект.* против конфигурации-источника не проверялись: не задан -ConfigPath"
} else {
$cfgRoot = $ConfigPath
if (-not [System.IO.Path]::IsPathRooted($cfgRoot)) { $cfgRoot = Join-Path (Get-Location).Path $cfgRoot }
if ((Test-Path $cfgRoot) -and -not (Test-Path $cfgRoot -PathType Container)) { $cfgRoot = Split-Path $cfgRoot -Parent }
if (-not (Test-Path (Join-Path $cfgRoot "Configuration.xml"))) {
Report-Warn "14. -ConfigPath '$ConfigPath': Configuration.xml не найден — проверка путей пропущена"
} else {
$check14Ok = $true
$pathCheckCount = 0
foreach ($bf in $script:borrowedFormsWithTree) {
$raw = $bf.RawText
$ctx = $bf.Context
$ownerKey = ($ctx -split '\.Form\.')[0]
$ownerParts = $ownerKey -split '\.', 2
if ($ownerParts.Count -lt 2) { continue }
$ownerType = $ownerParts[0]; $ownerName = $ownerParts[1]
$ownerDir = $childTypeDirMap[$ownerType]
if (-not $ownerDir) { continue }
$srcObjFile = Join-Path (Join-Path $cfgRoot $ownerDir) "${ownerName}.xml"
if (-not (Test-Path $srcObjFile)) {
Report-Warn "14. ${ctx}: объект-источник не найден в конфигурации ($ownerDir/${ownerName}.xml)"
continue
}
# Имена, доступные первым сегментом пути: реквизиты и ТЧ объекта-источника.
# Плюс для каждой ТЧ — её колонки: второй сегмент проверяем по ним (именно там
# и жил дефект — Объект.Товары.Артикул при живой ТЧ Товары).
$srcNames = @{}
$srcTSColumns = @{}
$srcDoc = New-Object System.Xml.XmlDocument
$srcDoc.PreserveWhitespace = $false
$srcDoc.Load($srcObjFile)
$srcObjEl = $null
foreach ($c in $srcDoc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $srcObjEl = $c; break }
}
$srcChildObjects = if ($srcObjEl) { $srcObjEl.SelectSingleNode("*[local-name()='ChildObjects']") } else { $null }
if ($srcChildObjects) {
foreach ($sub in $srcChildObjects.ChildNodes) {
if ($sub.NodeType -ne 'Element') { continue }
if ($sub.LocalName -notin @('Attribute','TabularSection')) { continue }
$nameNode = $sub.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
if (-not $nameNode) { continue }
$subName = $nameNode.InnerText.Trim()
$srcNames[$subName] = $true
if ($sub.LocalName -ne 'TabularSection') { continue }
$cols = @{}
foreach ($colName in $sub.SelectNodes("*[local-name()='ChildObjects']/*[local-name()='Attribute']/*[local-name()='Properties']/*[local-name()='Name']")) {
$cols[$colName.InnerText.Trim()] = $true
}
$srcTSColumns[$subName] = $cols
}
}
# Плюс колонки, объявленные в самой форме через <Columns>/<AdditionalColumns table="Объект.X">
foreach ($acm in [regex]::Matches($raw, '(?s)<AdditionalColumns table="Объект\.(\w+)">(.*?)</AdditionalColumns>')) {
$tbl = $acm.Groups[1].Value
if (-not $srcTSColumns.ContainsKey($tbl)) { $srcTSColumns[$tbl] = @{} }
foreach ($cm in [regex]::Matches($acm.Groups[2].Value, '<Column name="(\w+)"')) {
$srcTSColumns[$tbl][$cm.Groups[1].Value] = $true
}
}
$badPaths = @{}
foreach ($m in [regex]::Matches($raw, '<(?:\w+:)?\w*DataPath[^>]*>Объект\.([^<]+)</(?:\w+:)?\w*DataPath>')) {
$segments = $m.Groups[1].Value -split '\.'
$seg0 = $segments[0]
$pathCheckCount++
if ($script:standardObjectFields -contains $seg0) { continue }
if (-not $srcNames.ContainsKey($seg0)) {
$badPaths["Объект.${seg0}"] = "у ${ownerKey} нет такого реквизита или табличной части"
continue
}
# Второй сегмент проверяем только для табличных частей: у ссылочного реквизита
# он ведёт в чужой объект, и это уже другая проверка.
if ($segments.Count -lt 2 -or -not $srcTSColumns.ContainsKey($seg0)) { continue }
$seg1 = $segments[1]
if ($script:standardObjectFields -contains $seg1) { continue }
# Итог колонки — псевдополе платформы: Total<Колонка> при живой колонке законен
if ($seg1 -like "Total*" -and $srcTSColumns[$seg0].ContainsKey($seg1.Substring(5))) { continue }
if (-not $srcTSColumns[$seg0].ContainsKey($seg1)) {
$badPaths["Объект.${seg0}.${seg1}"] = "у табличной части ${seg0} нет колонки ${seg1}, и <Columns> формы её не объявляет"
}
}
foreach ($bad in ($badPaths.Keys | Sort-Object)) {
Report-Error "14. ${ctx}: путь '${bad}' — $($badPaths[$bad])"
$check14Ok = $false
}
}
if ($check14Ok) {
Report-OK "14. Object paths vs source config: $pathCheckCount checked"
}
}
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
$extRootDir = Split-Path $resolvedPath -Parent
$ctrlCount = 0
foreach ($bslFile in (Get-ChildItem -Path $extRootDir -Recurse -Filter *.bsl -File -ErrorAction SilentlyContinue)) {
$txt = [System.IO.File]::ReadAllText($bslFile.FullName, [System.Text.Encoding]::UTF8)
$ctrlCount += ([regex]::Matches($txt, '(?m)^\s*&ИзменениеИКонтроль\(')).Count
}
if ($ctrlCount -gt 0) {
Out-Line "[INFO] Контролируемых методов (&ИзменениеИКонтроль): $ctrlCount — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>"
}
# --- Final output ---
& $finalize
if ($script:errors -gt 0) {
exit 1
}
exit 0