Files
cc-1c-skills/.claude/skills/skd-validate/scripts/skd-validate.ps1
T
Nick ShirokovandClaude Opus 5.5 5465a7803c feat(skd-validate): проверка схемы компоновки по XSD платформы (#101)
Если в проекте есть схемы /v8-xsd-fetch, skd-validate дополнительно
проверяет порядок элементов, типы и значения по XSD версии формата
выгрузки (штамп Configuration.xml или корня внешнего отчёта). Схемы
точной версии — ошибки, только более новой — предупреждения, иначе
проверка пропускается. Места, которые платформа пишет иначе, чем
описывает XSD, отсекаются по месту нарушения: на 2606 схемах компоновки
выгрузок БП, УНФ и ERP (форматы 2.17–2.21) — ни одного срабатывания в
обоих портах.

Runner: поле кейса requires (путь от корня репозитория, нет — skipped)
и плейсхолдер {repoRoot} — кейсы на настоящих схемах, которые в
репозиторий не входят.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018dc2yLQfw6wppbopM1Y4Ps
2026-09-29 15:24:26 +03:00

1153 lines
40 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.
# skd-validate v1.5 — Validate 1C DCS structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory, Position=0)]
[Alias('Path')]
[string]$TemplatePath,
[switch]$Detailed,
[int]$MaxErrors = 20,
[string]$OutFile
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve path ---
if (-not [System.IO.Path]::IsPathRooted($TemplatePath)) {
$TemplatePath = Join-Path (Get-Location).Path $TemplatePath
}
# A: Directory → Ext/Template.xml
if (Test-Path $TemplatePath -PathType Container) {
$TemplatePath = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml"
}
# B1: Missing Ext/ (e.g. Templates/СКД/Template.xml → Templates/СКД/Ext/Template.xml)
if (-not (Test-Path $TemplatePath)) {
$fn = [System.IO.Path]::GetFileName($TemplatePath)
if ($fn -eq "Template.xml") {
$c = Join-Path (Join-Path (Split-Path $TemplatePath) "Ext") $fn
if (Test-Path $c) { $TemplatePath = $c }
}
}
# B2: Descriptor (Templates/СКД.xml → Templates/СКД/Ext/Template.xml)
if (-not (Test-Path $TemplatePath) -and $TemplatePath.EndsWith(".xml")) {
$stem = [System.IO.Path]::GetFileNameWithoutExtension($TemplatePath)
$dir = Split-Path $TemplatePath
$c = Join-Path (Join-Path (Join-Path $dir $stem) "Ext") "Template.xml"
if (Test-Path $c) { $TemplatePath = $c }
}
if (-not (Test-Path $TemplatePath)) {
Write-Error "File not found: $TemplatePath"
exit 1
}
$resolvedPath = (Resolve-Path $TemplatePath).Path
$fileName = [System.IO.Path]::GetFileName($resolvedPath)
# --- Output infrastructure ---
$script:errors = 0
$script:warnings = 0
$script:okCount = 0
$script:stopped = $false
$script:output = New-Object System.Text.StringBuilder 4096
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: $fileName ($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"
}
}
Out-Line "=== Validation: $fileName ==="
Out-Line ""
# --- 1. Parse XML ---
$xmlDoc = $null
try {
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $false
$xmlDoc.Load($resolvedPath)
Report-OK "XML parsed successfully"
} catch {
Report-Error "XML parse failed: $($_.Exception.Message)"
# Cannot continue
$result = $script:output.ToString()
Write-Host $result
if ($OutFile) {
$utf8Bom = New-Object System.Text.UTF8Encoding $true
[System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom)
}
exit 1
}
# --- 2. Register namespaces ---
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$ns.AddNamespace("s", "http://v8.1c.ru/8.1/data-composition-system/schema")
$ns.AddNamespace("dcscom", "http://v8.1c.ru/8.1/data-composition-system/common")
$ns.AddNamespace("dcscor", "http://v8.1c.ru/8.1/data-composition-system/core")
$ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings")
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui")
$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema")
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
$ns.AddNamespace("dcsat", "http://v8.1c.ru/8.1/data-composition-system/area-template")
$root = $xmlDoc.DocumentElement
# --- 3. Root element checks ---
if ($root.LocalName -ne "DataCompositionSchema") {
Report-Error "Root element is '$($root.LocalName)', expected 'DataCompositionSchema'"
} else {
Report-OK "Root element: DataCompositionSchema"
}
$expectedNs = "http://v8.1c.ru/8.1/data-composition-system/schema"
if ($root.NamespaceURI -ne $expectedNs) {
Report-Error "Default namespace is '$($root.NamespaceURI)', expected '$expectedNs'"
} else {
Report-OK "Default namespace correct"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- 4. Collect inventories ---
# DataSources
$dataSourceNodes = $root.SelectNodes("s:dataSource", $ns)
$dataSourceNames = @{}
foreach ($dsn in $dataSourceNodes) {
$name = $dsn.SelectSingleNode("s:name", $ns)
if ($name) { $dataSourceNames[$name.InnerText] = $true }
}
# DataSets (recursive for unions)
$dataSetNodes = $root.SelectNodes("s:dataSet", $ns)
$dataSetNames = @{}
$allFieldPaths = @{} # Global: dataPath → dataSet name
function Collect-DataSetFields {
param($dsNode, [string]$dsName)
$fields = $dsNode.SelectNodes("s:field", $ns)
$localPaths = @{}
foreach ($f in $fields) {
$dp = $f.SelectSingleNode("s:dataPath", $ns)
if ($dp) {
$path = $dp.InnerText
$localPaths[$path] = $true
$allFieldPaths[$path] = $dsName
}
}
# Union items
$items = $dsNode.SelectNodes("s:item", $ns)
foreach ($item in $items) {
$itemName = $item.SelectSingleNode("s:name", $ns)
if ($itemName) {
Collect-DataSetFields -dsNode $item -dsName $itemName.InnerText
}
}
return $localPaths
}
$dataSetFieldMap = @{} # dsName → hashtable of dataPath
foreach ($ds in $dataSetNodes) {
$nameNode = $ds.SelectSingleNode("s:name", $ns)
if ($nameNode) {
$dsName = $nameNode.InnerText
$dataSetNames[$dsName] = $true
$dataSetFieldMap[$dsName] = Collect-DataSetFields -dsNode $ds -dsName $dsName
}
}
# CalculatedFields
$calcFieldNodes = $root.SelectNodes("s:calculatedField", $ns)
$calcFieldPaths = @{}
foreach ($cf in $calcFieldNodes) {
$dp = $cf.SelectSingleNode("s:dataPath", $ns)
if ($dp) { $calcFieldPaths[$dp.InnerText] = $true }
}
# TotalFields
$totalFieldNodes = $root.SelectNodes("s:totalField", $ns)
# Parameters
$paramNodes = $root.SelectNodes("s:parameter", $ns)
$paramNames = @{}
foreach ($p in $paramNodes) {
$nameNode = $p.SelectSingleNode("s:name", $ns)
if ($nameNode) { $paramNames[$nameNode.InnerText] = $true }
}
# Templates
$templateNodes = $root.SelectNodes("s:template", $ns)
$templateNames = @{}
foreach ($t in $templateNodes) {
$nameNode = $t.SelectSingleNode("s:name", $ns)
if ($nameNode) { $templateNames[$nameNode.InnerText] = $true }
}
# GroupTemplates
$groupTemplateNodes = $root.SelectNodes("s:groupTemplate", $ns)
# SettingsVariants
$variantNodes = $root.SelectNodes("s:settingsVariant", $ns)
# Known fields = dataset fields + calculated fields
$knownFields = @{}
foreach ($key in $allFieldPaths.Keys) { $knownFields[$key] = $true }
foreach ($key in $calcFieldPaths.Keys) { $knownFields[$key] = $true }
# --- 5. DataSource checks ---
if ($dataSourceNodes.Count -eq 0) {
Report-Warn "No dataSource elements found (settings-only DCS?)"
} else {
$dsNamesSeen = @{}
$dsOk = $true
foreach ($dsn in $dataSourceNodes) {
$name = $dsn.SelectSingleNode("s:name", $ns)
$type = $dsn.SelectSingleNode("s:dataSourceType", $ns)
if (-not $name -or -not $name.InnerText) {
Report-Error "DataSource has empty name"
$dsOk = $false
} elseif ($dsNamesSeen.ContainsKey($name.InnerText)) {
Report-Error "Duplicate dataSource name: $($name.InnerText)"
$dsOk = $false
} else {
$dsNamesSeen[$name.InnerText] = $true
}
if ($type) {
$tv = $type.InnerText
if ($tv -ne "Local" -and $tv -ne "External") {
Report-Warn "DataSource '$($name.InnerText)' has unusual type: $tv"
}
}
}
if ($dsOk) {
Report-OK "$($dataSourceNodes.Count) dataSource(s) found, names unique"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- 6. DataSet checks ---
$validDsTypes = @("DataSetQuery", "DataSetObject", "DataSetUnion")
if ($dataSetNodes.Count -eq 0) {
Report-Warn "No dataSet elements found (settings-only DCS?)"
} else {
$dsNamesSeen = @{}
$dsOk = $true
foreach ($ds in $dataSetNodes) {
$xsiType = $ds.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
$nameNode = $ds.SelectSingleNode("s:name", $ns)
$dsName = if ($nameNode) { $nameNode.InnerText } else { "(unnamed)" }
if (-not $nameNode -or -not $nameNode.InnerText) {
Report-Error "DataSet has empty name"
$dsOk = $false
} elseif ($dsNamesSeen.ContainsKey($dsName)) {
Report-Error "Duplicate dataSet name: $dsName"
$dsOk = $false
} else {
$dsNamesSeen[$dsName] = $true
}
if (-not $xsiType) {
Report-Error "DataSet '$dsName' missing xsi:type"
$dsOk = $false
} elseif ($validDsTypes -notcontains $xsiType) {
Report-Warn "DataSet '$dsName' has unusual xsi:type: $xsiType"
}
# Check dataSource reference
if ($xsiType -ne "DataSetUnion") {
$srcNode = $ds.SelectSingleNode("s:dataSource", $ns)
if ($srcNode -and $srcNode.InnerText) {
if (-not $dataSourceNames.ContainsKey($srcNode.InnerText)) {
Report-Error "DataSet '$dsName' references unknown dataSource: $($srcNode.InnerText)"
$dsOk = $false
}
}
}
# Check query not empty for Query type
if ($xsiType -eq "DataSetQuery") {
$queryNode = $ds.SelectSingleNode("s:query", $ns)
if (-not $queryNode -or -not $queryNode.InnerText.Trim()) {
Report-Warn "DataSet '$dsName' (Query) has empty query"
}
}
# Check objectName for Object type
if ($xsiType -eq "DataSetObject") {
$objNode = $ds.SelectSingleNode("s:objectName", $ns)
if (-not $objNode -or -not $objNode.InnerText.Trim()) {
Report-Error "DataSet '$dsName' (Object) has empty objectName"
$dsOk = $false
}
}
}
if ($dsOk) {
Report-OK "$($dataSetNodes.Count) dataSet(s) found, names unique"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- 7. Field checks ---
function Check-DataSetFields {
param($dsNode, [string]$dsName)
$fields = $dsNode.SelectNodes("s:field", $ns)
if ($fields.Count -eq 0) { return }
$pathsSeen = @{}
$fieldOk = $true
foreach ($f in $fields) {
$dp = $f.SelectSingleNode("s:dataPath", $ns)
$fn = $f.SelectSingleNode("s:field", $ns)
if (-not $dp -or -not $dp.InnerText) {
Report-Error "DataSet '$dsName': field has empty dataPath"
$fieldOk = $false
continue
}
$path = $dp.InnerText
if ($pathsSeen.ContainsKey($path)) {
Report-Warn "DataSet '$dsName': duplicate dataPath '$path'"
} else {
$pathsSeen[$path] = $true
}
if (-not $fn -or -not $fn.InnerText) {
Report-Warn "DataSet '$dsName': field '$path' has empty <field> element"
}
}
if ($fieldOk) {
Report-OK "DataSet `"$dsName`": $($fields.Count) fields, dataPath unique"
}
# Check union items recursively
$items = $dsNode.SelectNodes("s:item", $ns)
foreach ($item in $items) {
$itemName = $item.SelectSingleNode("s:name", $ns)
$iName = if ($itemName) { $itemName.InnerText } else { "(unnamed item)" }
Check-DataSetFields -dsNode $item -dsName $iName
}
}
foreach ($ds in $dataSetNodes) {
$nameNode = $ds.SelectSingleNode("s:name", $ns)
$dsName = if ($nameNode) { $nameNode.InnerText } else { "(unnamed)" }
Check-DataSetFields -dsNode $ds -dsName $dsName
}
if ($script:stopped) { & $finalize; exit 1 }
# --- 8. DataSetLink checks ---
$linkNodes = $root.SelectNodes("s:dataSetLink", $ns)
if ($linkNodes.Count -gt 0) {
$linkOk = $true
foreach ($link in $linkNodes) {
$src = $link.SelectSingleNode("s:sourceDataSet", $ns)
$dst = $link.SelectSingleNode("s:destinationDataSet", $ns)
$srcExpr = $link.SelectSingleNode("s:sourceExpression", $ns)
$dstExpr = $link.SelectSingleNode("s:destinationExpression", $ns)
if ($src -and $src.InnerText -and -not $dataSetNames.ContainsKey($src.InnerText)) {
Report-Error "DataSetLink: sourceDataSet '$($src.InnerText)' not found"
$linkOk = $false
}
if ($dst -and $dst.InnerText -and -not $dataSetNames.ContainsKey($dst.InnerText)) {
Report-Error "DataSetLink: destinationDataSet '$($dst.InnerText)' not found"
$linkOk = $false
}
if (-not $srcExpr -or -not $srcExpr.InnerText.Trim()) {
Report-Error "DataSetLink: empty sourceExpression"
$linkOk = $false
}
if (-not $dstExpr -or -not $dstExpr.InnerText.Trim()) {
Report-Error "DataSetLink: empty destinationExpression"
$linkOk = $false
}
}
if ($linkOk) {
Report-OK "$($linkNodes.Count) dataSetLink(s): references valid"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- 9. CalculatedField checks ---
if ($calcFieldNodes.Count -gt 0) {
$cfOk = $true
$cfSeen = @{}
# Collect totalField dataPaths — an empty calculatedField is legitimate if a
# totalField with the same dataPath provides the expression (real-world
# pattern in vendor ERP/БП reports for fields visible only in totals).
$tfPaths = @{}
foreach ($tf in $totalFieldNodes) {
$tfDp = $tf.SelectSingleNode("s:dataPath", $ns)
if ($tfDp -and $tfDp.InnerText) {
$tfPaths[$tfDp.InnerText] = $true
}
}
foreach ($cf in $calcFieldNodes) {
$dp = $cf.SelectSingleNode("s:dataPath", $ns)
$expr = $cf.SelectSingleNode("s:expression", $ns)
if (-not $dp -or -not $dp.InnerText) {
Report-Error "CalculatedField has empty dataPath"
$cfOk = $false
continue
}
$path = $dp.InnerText
if ($cfSeen.ContainsKey($path)) {
Report-Error "Duplicate calculatedField dataPath: $path"
$cfOk = $false
} else {
$cfSeen[$path] = $true
}
if (-not $expr -or -not $expr.InnerText.Trim()) {
# Empty expression is legitimate in several vendor patterns:
# - totalField with same dataPath provides the calculation
# - groupTemplate uses the field as group name (declarative only)
# - field is referenced only by settingsVariants for grouping
# Surface as warning, not error, to avoid false positives on real
# ERP/БП reports while still flagging the unusual shape.
if (-not $tfPaths.ContainsKey($path)) {
Report-Warn "CalculatedField '$path' has empty expression (declarative-only?)"
}
}
# Warn if collides with a dataset field
if ($allFieldPaths.ContainsKey($path)) {
Report-Warn "CalculatedField '$path' shadows dataSet field in '$($allFieldPaths[$path])'"
}
}
if ($cfOk) {
Report-OK "$($calcFieldNodes.Count) calculatedField(s): dataPath and expression valid"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- 10. TotalField checks ---
if ($totalFieldNodes.Count -gt 0) {
$tfOk = $true
foreach ($tf in $totalFieldNodes) {
$dp = $tf.SelectSingleNode("s:dataPath", $ns)
$expr = $tf.SelectSingleNode("s:expression", $ns)
if (-not $dp -or -not $dp.InnerText) {
Report-Error "TotalField has empty dataPath"
$tfOk = $false
continue
}
if (-not $expr -or -not $expr.InnerText.Trim()) {
Report-Error "TotalField '$($dp.InnerText)' has empty expression"
$tfOk = $false
}
}
if ($tfOk) {
Report-OK "$($totalFieldNodes.Count) totalField(s): dataPath and expression present"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- 11. Parameter checks ---
if ($paramNodes.Count -gt 0) {
$paramOk = $true
$paramSeen = @{}
foreach ($p in $paramNodes) {
$nameNode = $p.SelectSingleNode("s:name", $ns)
if (-not $nameNode -or -not $nameNode.InnerText) {
Report-Error "Parameter has empty name"
$paramOk = $false
continue
}
$pName = $nameNode.InnerText
if ($paramSeen.ContainsKey($pName)) {
Report-Error "Duplicate parameter name: $pName"
$paramOk = $false
} else {
$paramSeen[$pName] = $true
}
}
if ($paramOk) {
Report-OK "$($paramNodes.Count) parameter(s): names unique"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- 12. Template checks ---
if ($templateNodes.Count -gt 0) {
$tplOk = $true
$tplSeen = @{}
foreach ($t in $templateNodes) {
$nameNode = $t.SelectSingleNode("s:name", $ns)
if (-not $nameNode -or -not $nameNode.InnerText) {
Report-Error "Template has empty name"
$tplOk = $false
continue
}
$tName = $nameNode.InnerText
if ($tplSeen.ContainsKey($tName)) {
# Vendor configs (ERP/БП) ship templates with repeating names — the
# platform identifies them by position/context, not by <name>. Demote
# to warning so the check still surfaces the collision without failing.
Report-Warn "Duplicate template name: $tName (allowed by platform but ambiguous)"
} else {
$tplSeen[$tName] = $true
}
}
if ($tplOk) {
Report-OK "$($templateNodes.Count) template(s) found"
}
}
# --- 13. GroupTemplate checks ---
if ($groupTemplateNodes.Count -gt 0) {
$gtOk = $true
$validTplTypes = @("Header", "Footer", "Overall", "OverallHeader", "OverallFooter")
foreach ($gt in $groupTemplateNodes) {
$tplRef = $gt.SelectSingleNode("s:template", $ns)
$tplType = $gt.SelectSingleNode("s:templateType", $ns)
if ($tplRef -and $tplRef.InnerText -and -not $templateNames.ContainsKey($tplRef.InnerText)) {
Report-Error "GroupTemplate references unknown template: $($tplRef.InnerText)"
$gtOk = $false
}
if ($tplType -and $validTplTypes -notcontains $tplType.InnerText) {
Report-Warn "GroupTemplate has unusual templateType: $($tplType.InnerText)"
}
}
if ($gtOk) {
Report-OK "$($groupTemplateNodes.Count) groupTemplate(s): references valid"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- 14. Settings helper functions ---
$validComparisonTypes = @(
"Equal","NotEqual","Greater","GreaterOrEqual","Less","LessOrEqual",
"InList","NotInList","InHierarchy","NotInHierarchy",
"InListByHierarchy","NotInListByHierarchy",
"Contains","NotContains","BeginsWith","NotBeginsWith",
"Filled","NotFilled"
)
$validStructureTypes = @(
"dcsset:StructureItemGroup",
"dcsset:StructureItemTable",
"dcsset:StructureItemChart",
"dcsset:StructureItemNestedObject"
)
function Check-FilterItems {
param($parentNode, [string]$variantName)
$filterItems = $parentNode.SelectNodes("dcsset:filter/dcsset:item", $ns)
foreach ($fi in $filterItems) {
if ($script:stopped) { return }
$xsiType = $fi.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
if ($xsiType -eq "dcsset:FilterItemComparison") {
$compType = $fi.SelectSingleNode("dcsset:comparisonType", $ns)
if ($compType -and $validComparisonTypes -notcontains $compType.InnerText) {
Report-Error "Variant '$variantName' filter: invalid comparisonType '$($compType.InnerText)'"
}
} elseif ($xsiType -eq "dcsset:FilterItemGroup") {
$groupType = $fi.SelectSingleNode("dcsset:groupType", $ns)
if ($groupType) {
$validGroupTypes = @("AndGroup","OrGroup","NotGroup")
if ($validGroupTypes -notcontains $groupType.InnerText) {
Report-Warn "Variant '$variantName' filter group: unusual groupType '$($groupType.InnerText)'"
}
}
# Recurse into nested items
$nestedItems = $fi.SelectNodes("dcsset:item", $ns)
foreach ($ni in $nestedItems) {
$niType = $ni.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
if ($niType -eq "dcsset:FilterItemComparison") {
$compType = $ni.SelectSingleNode("dcsset:comparisonType", $ns)
if ($compType -and $validComparisonTypes -notcontains $compType.InnerText) {
Report-Error "Variant '$variantName' filter: invalid comparisonType '$($compType.InnerText)'"
}
}
}
}
}
}
function Check-StructureItem {
param($itemNode, [string]$variantName)
if ($script:stopped) { return }
$xsiType = $itemNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
if (-not $xsiType) {
Report-Error "Variant '$variantName': structure item missing xsi:type"
return
}
if ($validStructureTypes -notcontains $xsiType) {
Report-Warn "Variant '$variantName': unusual structure item type '$xsiType'"
}
# Recurse into nested items (groups can contain groups)
$nestedItems = $itemNode.SelectNodes("dcsset:item", $ns)
foreach ($ni in $nestedItems) {
Check-StructureItem -itemNode $ni -variantName $variantName
}
# Check column/row in tables
if ($xsiType -eq "dcsset:StructureItemTable") {
$columns = $itemNode.SelectNodes("dcsset:column", $ns)
$rows = $itemNode.SelectNodes("dcsset:row", $ns)
if ($columns.Count -eq 0) {
Report-Warn "Variant '$variantName': table has no columns"
}
if ($rows.Count -eq 0) {
Report-Warn "Variant '$variantName': table has no rows"
}
}
}
function Check-Settings {
param($settingsNode, [string]$variantName)
if ($script:stopped) { return }
# Selection
$selItems = $settingsNode.SelectNodes("dcsset:selection/dcsset:item", $ns)
foreach ($si in $selItems) {
$xsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
if ($xsiType -eq "dcsset:SelectedItemField") {
$field = $si.SelectSingleNode("dcsset:field", $ns)
if ($field -and $field.InnerText -and $field.InnerText -ne "SystemFields.Number") {
$basePath = ($field.InnerText -split '\.')[0]
if (-not $knownFields.ContainsKey($field.InnerText) -and -not $knownFields.ContainsKey($basePath)) {
# Soft check — autoFillFields may add fields not listed explicitly
}
}
}
}
# Filter
Check-FilterItems -parentNode $settingsNode -variantName $variantName
# Order
$orderItems = $settingsNode.SelectNodes("dcsset:order/dcsset:item", $ns)
foreach ($oi in $orderItems) {
$xsiType = $oi.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
if ($xsiType -eq "dcsset:OrderItemField") {
$orderType = $oi.SelectSingleNode("dcsset:orderType", $ns)
if ($orderType -and $orderType.InnerText -ne "Asc" -and $orderType.InnerText -ne "Desc") {
Report-Warn "Variant '$variantName' order: invalid orderType '$($orderType.InnerText)'"
}
}
}
# Structure items
$structItems = $settingsNode.SelectNodes("dcsset:item", $ns)
foreach ($si in $structItems) {
Check-StructureItem -itemNode $si -variantName $variantName
}
}
# --- 15. SettingsVariant checks ---
if ($variantNodes.Count -eq 0) {
Report-Warn "No settingsVariant elements found"
} else {
$vOk = $true
$vIdx = 0
foreach ($v in $variantNodes) {
$vIdx++
$vName = $v.SelectSingleNode("dcsset:name", $ns)
if (-not $vName -or -not $vName.InnerText) {
Report-Error "SettingsVariant #$vIdx has empty name"
$vOk = $false
}
$settings = $v.SelectSingleNode("dcsset:settings", $ns)
if (-not $settings) {
Report-Error "SettingsVariant '$($vName.InnerText)' has no settings element"
$vOk = $false
continue
}
# Check settings internals
Check-Settings -settingsNode $settings -variantName "$($vName.InnerText)"
}
if ($vOk) {
Report-OK "$($variantNodes.Count) settingsVariant(s) found"
}
}
# --- 16. valueType structural checks ---
# Catches broken XDTO that XML/structural checks miss (decimal without xs:,
# missing qualifiers, mismatched qualifier blocks, unknown sign/length tokens).
$validTypeQualifier = @{
'xs:decimal' = 'v8:NumberQualifiers'
'xs:string' = 'v8:StringQualifiers'
'xs:dateTime' = 'v8:DateQualifiers'
'xs:boolean' = ''
'v8:StandardPeriod' = ''
'v8:UUID' = ''
'v8:Null' = ''
'v8:Type' = ''
'v8:ValueStorage' = ''
}
$validSign = @('Any', 'Nonnegative', 'Negative')
$validLength = @('Variable', 'Fixed')
$validFractions = @('Date', 'DateTime', 'Time')
# DCS supports composite types: multiple <v8:Type> blocks may share a single
# trailing qualifier block (e.g. xs:string + CatalogRef.X + StringQualifiers).
# So we collect all types and qualifiers per valueType, then check consistency.
$qualifierProducers = @{
'v8:NumberQualifiers' = 'xs:decimal'
'v8:StringQualifiers' = 'xs:string'
'v8:DateQualifiers' = 'xs:dateTime'
}
$valueTypeNodes = $root.SelectNodes("//s:valueType", $ns)
$vtChecked = 0
$vtOk = $true
foreach ($vt in $valueTypeNodes) {
$vtChecked++
$types = @() # list of short type strings; '' marks a ref type
$qualifiers = @() # list of @{ name = 'v8:XQualifiers'; node = $child }
foreach ($child in $vt.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
if ($child.NamespaceURI -ne 'http://v8.1c.ru/8.1/data/core') { continue }
$localName = $child.LocalName
if ($localName -eq 'Type') {
$t = "$($child.InnerText)".Trim()
if (-not $t) {
Report-Error "valueType: <v8:Type> is empty"
$vtOk = $false
continue
}
if ($t -match '^([A-Za-z][A-Za-z0-9]*):(.+)$') {
$prefix = $Matches[1]
$localT = $Matches[2]
if ($prefix -eq 'xs' -or $prefix -eq 'v8') {
if (-not $validTypeQualifier.ContainsKey($t)) {
Report-Error "valueType: unknown type '$t' (allowed: xs:decimal/xs:string/xs:dateTime/xs:boolean/v8:StandardPeriod or <prefix>:*Ref.X)"
$vtOk = $false
} else {
$types += $t
}
} else {
$prefixNs = $child.GetNamespaceOfPrefix($prefix)
if ($prefixNs -eq 'http://v8.1c.ru/8.1/data/enterprise/current-config') {
if (-not ($localT -match '^[A-Za-z]+(Ref)?\.')) {
Report-Error "valueType: ref type '$t' must look like '<prefix>:<Kind>.<Name>' (e.g. d5p1:CatalogRef.X)"
$vtOk = $false
} else {
$types += '' # ref — no qualifier needed
}
} elseif ($prefixNs -eq 'http://v8.1c.ru/8.1/data/enterprise') {
# System types: AccumulationRecordType etc. — no qualifiers
if (-not ($localT -match '^[A-Za-z][A-Za-z0-9]*$')) {
Report-Error "valueType: system type '$t' has unexpected local-name shape"
$vtOk = $false
} else {
$types += ''
}
} else {
Report-Error "valueType: type '$t' uses prefix '$prefix' bound to unexpected namespace '$prefixNs'"
$vtOk = $false
}
}
} else {
Report-Error "valueType: type '$t' has no namespace prefix (expected xs:/v8:/d5p1: — e.g. xs:decimal not decimal)"
$vtOk = $false
}
} elseif ($localName -match 'Qualifiers$') {
$qName = "v8:$localName"
$qualifiers += @{ name = $qName; node = $child }
# Validate qualifier internals
if ($qName -eq 'v8:NumberQualifiers') {
$digits = $child.SelectSingleNode("v8:Digits", $ns)
$frac = $child.SelectSingleNode("v8:FractionDigits", $ns)
$sign = $child.SelectSingleNode("v8:AllowedSign", $ns)
if (-not $digits -or -not ($digits.InnerText -match '^\d+$')) {
Report-Error "v8:NumberQualifiers: <v8:Digits> missing or not a non-negative integer"
$vtOk = $false
}
if (-not $frac -or -not ($frac.InnerText -match '^\d+$')) {
Report-Error "v8:NumberQualifiers: <v8:FractionDigits> missing or not a non-negative integer"
$vtOk = $false
}
if ($sign -and $sign.InnerText -and $sign.InnerText -notin $validSign) {
Report-Error "v8:NumberQualifiers: <v8:AllowedSign>$($sign.InnerText)</v8:AllowedSign> — must be one of: $($validSign -join ', ')"
$vtOk = $false
}
} elseif ($qName -eq 'v8:StringQualifiers') {
$len = $child.SelectSingleNode("v8:Length", $ns)
$al = $child.SelectSingleNode("v8:AllowedLength", $ns)
if (-not $len -or -not ($len.InnerText -match '^\d+$')) {
Report-Error "v8:StringQualifiers: <v8:Length> missing or not a non-negative integer"
$vtOk = $false
}
if ($al -and $al.InnerText -and $al.InnerText -notin $validLength) {
Report-Error "v8:StringQualifiers: <v8:AllowedLength>$($al.InnerText)</v8:AllowedLength> — must be one of: $($validLength -join ', ')"
$vtOk = $false
}
} elseif ($qName -eq 'v8:DateQualifiers') {
$df = $child.SelectSingleNode("v8:DateFractions", $ns)
if ($df -and $df.InnerText -and $df.InnerText -notin $validFractions) {
Report-Error "v8:DateQualifiers: <v8:DateFractions>$($df.InnerText)</v8:DateFractions> — must be one of: $($validFractions -join ', ')"
$vtOk = $false
}
}
}
}
# Cross-check: every qualifier must have a matching scalar type in this valueType
foreach ($q in $qualifiers) {
$producer = $qualifierProducers[$q.name]
if (-not $producer) { continue }
if ($types -notcontains $producer) {
Report-Error "valueType: <$($q.name)> has no matching <v8:Type>$producer</v8:Type> in this valueType"
$vtOk = $false
}
}
}
if ($vtChecked -gt 0 -and $vtOk) {
Report-OK "$vtChecked valueType block(s): structure and qualifiers OK"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- 17. value content checks ---
# Catches literal placeholders ("_") and empty strings in DesignTimeValue refs
# that XDTO would reject at db-load-xml.
$valueNodes = @()
$valueNodes += @($root.SelectNodes("//s:value[@xsi:type]", $ns))
$valueNodes += @($root.SelectNodes("//dcscor:value[@xsi:type]", $ns))
$vChecked = 0
$vOk = $true
foreach ($vn in $valueNodes) {
if (-not $vn) { continue }
$vChecked++
$xsiType = $vn.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
$text = $vn.InnerText
if ($xsiType -eq 'dcscor:DesignTimeValue') {
if (-not $text -or $text.Trim() -eq '' -or $text.Trim() -eq '_') {
Report-Error "<value xsi:type=`"dcscor:DesignTimeValue`">$text</value> — DesignTimeValue must be a reference path (e.g. Перечисление.X.Y), not '$text'"
$vOk = $false
} elseif (-not ($text -match '^[A-Za-zА-Яа-яЁё]+\.[A-Za-zА-Яа-яЁё0-9_]+')) {
Report-Warn "<value xsi:type=`"dcscor:DesignTimeValue`">$text</value> — doesn't look like a typical ref path"
}
}
}
if ($vChecked -gt 0 -and $vOk) {
Report-OK "$vChecked <value> element(s) with xsi:type: content OK"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- 18. XSD (schemas of the platform, /v8-xsd-fetch) ---
# Порядок элементов, типы и значения — по XSD платформы, если они есть в проекте. Нет схем —
# нет проверки. Схемы XDTO совпадают с выгрузкой конфигуратора только в СКД, и то кроме
# нескольких мест, которые платформа пишет иначе, — они отсекаются ниже (Test-XsdPlatformNoise).
# Копия общего эталона (семья find_v8project, авторитет — cf-edit).
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
# Штамп версии формата — атрибут version КОРНЕВОГО элемента файла. Именно корневого: в Form.xml
# расширения ниже стоит <BaseForm version=…>. Читается только заголовок, без разбора файла.
function Get-RootVersion([string]$xmlPath) {
if (-not (Test-Path -LiteralPath $xmlPath -PathType Leaf)) { return $null }
$buf = New-Object byte[] 4096
$fs = [System.IO.File]::OpenRead($xmlPath)
try { $len = $fs.Read($buf, 0, $buf.Length) } finally { $fs.Dispose() }
$head = [System.Text.Encoding]::UTF8.GetString($buf, 0, $len)
$m = [regex]::Match($head, '<[A-Za-z_][\w.:-]*(\s[^>]*)?/?>')
if (-not $m.Success) { return $null }
$v = [regex]::Match($m.Groups[1].Value, '(?:^|\s)version="([^"]*)"')
if ($v.Success) { return $v.Groups[1].Value }
return $null
}
# Корень автономной внешней обработки/отчёта. Копия общего эталона (семья is_external_root,
# авторитет — cf-edit).
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
# Якорь выгрузки, в чьём дереве лежит файл: корень автономной EPF/ERF либо Configuration.xml,
# ближайший вверх. Корень обработки проверяется первым — иначе обработка, лежащая в дереве
# конфигурации, сверялась бы с конфигурацией. Нет якоря — сверять не с чем.
function Find-DumpAnchor([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 15 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return "$d.xml" }
$cfg = Join-Path $d "Configuration.xml"
if (Test-Path $cfg) { return $cfg }
$parent = [System.IO.Path]::GetDirectoryName($d)
if (-not $parent -or $parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
$xsdDcsNs = "http://v8.1c.ru/8.1/data-composition-system/schema"
# targetNamespace схемы — из заголовка файла, без разбора
function Get-XsdTargetNs([string]$path) {
$buf = New-Object byte[] 4096
$fs = [System.IO.File]::OpenRead($path)
try { $len = $fs.Read($buf, 0, $buf.Length) } finally { $fs.Dispose() }
$m = [regex]::Match([System.Text.Encoding]::UTF8.GetString($buf, 0, $len), 'targetNamespace="([^"]*)"')
if ($m.Success) { return $m.Groups[1].Value }
return $null
}
# Каталог версии годится, если в нём есть схема СКД (в 2.10–2.11 её нет)
function Test-XsdVersionDir([string]$dir) {
foreach ($f in Get-ChildItem -LiteralPath $dir -Filter *.xsd -File -ErrorAction SilentlyContinue) {
if ((Get-XsdTargetNs $f.FullName) -ceq $xsdDcsNs) { return $true }
}
return $false
}
# Каталог схем: .v8-project.json — сначала от шаблона (проект, которому он принадлежит), потом
# от текущего каталога; xsdPath из него, иначе .v8-xsd рядом с ним. Нет проекта — нет схем.
function Resolve-XsdRoot([string]$templateDir) {
$pj = Find-V8Project $templateDir
if (-not $pj) { $pj = Find-V8Project (Get-Location).Path }
if (-not $pj) { return $null }
$pjDir = [System.IO.Path]::GetDirectoryName($pj)
$rel = ".v8-xsd"
try {
$cfg = Get-Content -LiteralPath $pj -Raw -Encoding UTF8 | ConvertFrom-Json
if ($cfg.xsdPath) { $rel = "$($cfg.xsdPath)" }
} catch {}
$dir = if ([System.IO.Path]::IsPathRooted($rel)) { $rel } else { Join-Path $pjDir $rel }
if (Test-Path -LiteralPath $dir -PathType Container) { return $dir }
return $null
}
# Места, которые платформа пишет не так, как описывает XSD, — сверено на всех схемах компоновки
# выгрузок БП и УНФ. Правило — по месту (элемент, родитель, xsi:type), не по тексту: текст
# зависит от движка и языка.
function Test-XsdPlatformNoise($info) {
$e = $info.elem; $p = $info.parent
if (($p -ceq 'groupTemplate' -or $p -ceq 'groupHeaderTemplate') -and ($e -ceq 'templateType' -or $e -ceq 'groupName')) { return $true }
if ($p -ceq 'right' -and $e -ceq 'lastId') { return $true }
if ($p -ceq 'nestedSchema' -and $e -ceq 'schema') { return $true }
if ($info.xsiNs -ceq 'http://v8.1c.ru/8.2/data/chart') { return $true }
# Пустое выражение параметра макета платформа не пишет, схема требует
if ($e -ceq 'parameter' -and $info.xsiLocal -ceq 'ExpressionAreaTemplateParameter') { return $true }
return $false
}
function Invoke-XsdCheck {
$xsdRoot = Resolve-XsdRoot ([System.IO.Path]::GetDirectoryName($resolvedPath))
if (-not $xsdRoot) { Report-OK "XSD: no schemas in project — not checked"; return }
$anchor = Find-DumpAnchor ([System.IO.Path]::GetDirectoryName($resolvedPath))
$ver = if ($anchor) { Get-RootVersion $anchor } else { $null }
if (-not $ver -or (Get-FormatRank $ver) -eq 0) { Report-OK "XSD: format version unknown (template outside a dump) — not checked"; return }
# Точная версия — ошибки; ближайшая более новая — предупреждения; только старше — пропуск
$cands = @(Get-ChildItem -LiteralPath $xsdRoot -Directory |
Where-Object { $_.Name -match '^\d+\.\d+$' -and (Get-FormatRank $_.Name) -ge (Get-FormatRank $ver) } |
Sort-Object { Get-FormatRank $_.Name } |
Where-Object { Test-XsdVersionDir $_.FullName })
if ($cands.Count -eq 0) { Report-Warn "XSD: no schemas for format $ver in '$xsdRoot' — not checked"; return }
$useDir = $cands[0].FullName
$useVer = $cands[0].Name
$exact = ($useVer -eq $ver)
# Набор схем. У import нет schemaLocation — связь по namespace, поэтому в набор кладём все.
# Корень DataCompositionSchema в схеме объявлен как dataCompositionSchema (строчная) —
# дописываем элемент в саму схему СКД: вторую схему того же namespace без SourceUri
# XmlSchemaSet молча отбрасывает.
$schemas = @()
foreach ($f in Get-ChildItem -LiteralPath $useDir -Filter *.xsd -File) {
$fs = [System.IO.File]::OpenRead($f.FullName)
try { $schemas += [System.Xml.Schema.XmlSchema]::Read($fs, $null) } finally { $fs.Dispose() }
}
$dcs = $schemas | Where-Object { $_.TargetNamespace -ceq $xsdDcsNs } | Select-Object -First 1
$hasRoot = $false
foreach ($it in $dcs.Items) {
if ($it -is [System.Xml.Schema.XmlSchemaElement] -and $it.Name -ceq 'DataCompositionSchema') { $hasRoot = $true }
}
if (-not $hasRoot) {
$el = New-Object System.Xml.Schema.XmlSchemaElement
$el.Name = 'DataCompositionSchema'
$el.SchemaTypeName = New-Object System.Xml.XmlQualifiedName('DataCompositionSchema', $xsdDcsNs)
[void]$dcs.Items.Add($el)
}
$set = New-Object System.Xml.Schema.XmlSchemaSet
$set.XmlResolver = $null
foreach ($sc in $schemas) { [void]$set.Add($sc) }
try { $set.Compile() } catch { Report-Warn "XSD: schemas in '$useDir' do not compile — not checked: $($_.Exception.Message)"; return }
# Своя цепочка открытых элементов: в обработчике нужен родитель нарушителя
$script:xsdStack = New-Object System.Collections.ArrayList
$script:xsdFound = 0
$script:xsdTag = if ($exact) { "XSD $useVer" } else { "XSD $useVer (no schemas for $ver)" }
$script:xsdExact = $exact
$handler = {
param($sender, $e)
if ($script:stopped) { return }
$st = $script:xsdStack
if ($sender.NodeType -eq 'Element') {
$xt = $sender.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
$info = @{ elem = $sender.LocalName; parent = $(if ($st.Count -gt 0) { $st[$st.Count - 1].name } else { '' }); xsiNs = ''; xsiLocal = '' }
if ($xt) {
$pp = $xt.Split(':')
$info.xsiLocal = $pp[-1]
if ($pp.Count -gt 1) { $info.xsiNs = "$($sender.LookupNamespace($pp[0]))" }
}
} else {
$top = if ($st.Count -gt 0) { $st[$st.Count - 1] } else { @{ name = ''; xsiNs = ''; xsiLocal = '' } }
$info = @{ elem = $top.name; parent = $(if ($st.Count -gt 1) { $st[$st.Count - 2].name } else { '' }); xsiNs = $top.xsiNs; xsiLocal = $top.xsiLocal }
}
if (Test-XsdPlatformNoise $info) { return }
$script:xsdFound++
$msg = "$($script:xsdTag), line $($e.Exception.LineNumber): $($info.parent)/$($info.elem): $($e.Message -replace '[\r\n]+', ' ')"
if ($script:xsdExact) { Report-Error $msg } else { Report-Warn $msg }
}
$rs = New-Object System.Xml.XmlReaderSettings
$rs.ValidationType = 'Schema'
$rs.Schemas = $set
$rs.DtdProcessing = 'Prohibit'
$rs.add_ValidationEventHandler($handler)
$r = [System.Xml.XmlReader]::Create($resolvedPath, $rs)
try {
while ($r.Read()) {
if ($r.NodeType -eq 'Element' -and -not $r.IsEmptyElement) {
$xt = $r.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
$entry = @{ name = $r.LocalName; xsiNs = ''; xsiLocal = '' }
if ($xt) {
$pp = $xt.Split(':')
$entry.xsiLocal = $pp[-1]
if ($pp.Count -gt 1) { $entry.xsiNs = "$($r.LookupNamespace($pp[0]))" }
}
[void]$script:xsdStack.Add($entry)
} elseif ($r.NodeType -eq 'EndElement' -and $script:xsdStack.Count -gt 0) {
$script:xsdStack.RemoveAt($script:xsdStack.Count - 1)
}
if ($script:stopped) { break }
}
} finally { $r.Dispose() }
if ($script:xsdFound -eq 0) { Report-OK "$($script:xsdTag): order, types and values OK" }
}
Invoke-XsdCheck
if ($script:stopped) { & $finalize; exit 1 }
# --- Final output ---
& $finalize
if ($script:errors -gt 0) {
exit 1
}
exit 0