Add skd-compile and skd-validate skills for DCS schema generation

- skd-compile: JSON DSL → Template.xml (DataCompositionSchema)
  Shorthand parsers for fields, totals, parameters, calculated fields.
  Full type system, settings variants with selection/filter/order/structure.
- skd-validate: structural validation of Template.xml (~30 checks)
  DataSources, DataSets, fields, links, params, templates, variants.
- docs/skd-dsl-spec.md: full DSL specification

Tested on compiled examples and 5+ real DCS from acc_8.3.24 (0 errors).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-02-10 21:27:19 +03:00
parent c306d18648
commit d573f84c9d
5 changed files with 2746 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
---
name: skd-compile
description: Компиляция схемы компоновки данных 1С (СКД) — Template.xml из компактного JSON-определения
argument-hint: <JsonPath> <OutputPath>
allowed-tools:
- Bash
- Read
- Write
- Glob
---
# /skd-compile — генерация СКД из JSON DSL
Принимает JSON-определение схемы компоновки данных → генерирует Template.xml (DataCompositionSchema).
## Параметры и команда
| Параметр | Описание |
|----------|----------|
| `JsonPath` | Путь к JSON-определению СКД |
| `OutputPath` | Путь к выходному Template.xml |
```powershell
powershell.exe -NoProfile -File .claude\skills\skd-compile\scripts\skd-compile.ps1 -JsonPath "<json>" -OutputPath "<Template.xml>"
```
## JSON DSL — краткий справочник
Полная спецификация: `docs/skd-dsl-spec.md`.
### Корневая структура
```json
{
"dataSets": [...],
"calculatedFields": [...],
"totalFields": [...],
"parameters": [...],
"dataSetLinks": [...],
"settingsVariants": [...]
}
```
Умолчания: `dataSources` → авто `ИсточникДанных1/Local`; `settingsVariants` → авто "Основной" с деталями.
### Наборы данных
Тип по ключу: `query` → DataSetQuery, `objectName` → DataSetObject, `items` → DataSetUnion.
```json
{ "name": "Продажи", "query": "ВЫБРАТЬ ...", "fields": [...] }
```
### Поля — shorthand
```
"Наименование" — просто имя
"Количество: decimal(15,2)" — имя + тип
"Организация: CatalogRef.Организации @dimension" — + роль
"Служебное: string #noFilter #noOrder" — + ограничения
```
Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`.
Роли: `@dimension`, `@account`, `@balance`, `@period`.
Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`.
### Итоги (shorthand)
```json
"totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"]
```
### Параметры (shorthand)
```json
"parameters": ["Период: StandardPeriod = LastMonth", "Организация: CatalogRef.Организации"]
```
### Варианты настроек
```json
"settingsVariants": [{
"name": "Основной",
"presentation": "Основной",
"settings": {
"selection": ["Наименование", "Количество", "Auto"],
"filter": [{ "field": "Организация", "op": "=", "use": false, "userSettingID": "auto" }],
"order": ["Количество desc", "Auto"],
"outputParameters": { "Заголовок": "Мой отчёт" },
"structure": [{ "type": "group", "groupBy": ["Организация"], "selection": ["Auto"], "order": ["Auto"],
"children": [{ "type": "group", "selection": ["Auto"], "order": ["Auto"] }]
}]
}
}]
```
## Примеры
### Минимальный
```json
{
"dataSets": [{
"query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура",
"fields": ["Наименование"]
}]
}
```
### С ресурсами и параметрами
```json
{
"dataSets": [{
"query": "ВЫБРАТЬ Продажи.Номенклатура, Продажи.Количество, Продажи.Сумма ИЗ РегистрНакопления.Продажи КАК Продажи",
"fields": ["Номенклатура: CatalogRef.Номенклатура @dimension", "Количество: decimal(15,3)", "Сумма: decimal(15,2)"]
}],
"totalFields": ["Количество: Сумма", "Сумма: Сумма"],
"parameters": ["Период: StandardPeriod = LastMonth"]
}
```
## Верификация
```
/skd-validate <OutputPath> — валидация структуры XML
/skd-info <OutputPath> — визуальная сводка
/skd-info <OutputPath> -Mode variant -Name 1 — проверка варианта настроек
```
File diff suppressed because it is too large Load Diff
+74
View File
@@ -0,0 +1,74 @@
---
name: skd-validate
description: Валидация структурной корректности схемы компоновки данных 1С (СКД) — Template.xml
argument-hint: <TemplatePath> [-MaxErrors 20]
allowed-tools:
- Bash
- Read
- Glob
---
# /skd-validate — валидация СКД (DataCompositionSchema)
Проверяет структурную корректность Template.xml схемы компоновки данных. Выявляет ошибки формата, битые ссылки, дубликаты имён.
## Параметры и команда
| Параметр | Описание |
|----------|----------|
| `TemplatePath` | Путь к Template.xml или каталогу макета (авто-резолв в `Ext/Template.xml`) |
| `MaxErrors` | Макс. ошибок до остановки (по умолчанию 20) |
| `OutFile` | Записать результат в файл |
```powershell
powershell.exe -NoProfile -File .claude\skills\skd-validate\scripts\skd-validate.ps1 -TemplatePath "<путь>"
```
## Проверки (~30)
| Группа | Что проверяется |
|--------|-----------------|
| **Root** | XML parse, корневой элемент `DataCompositionSchema`, default namespace, ns-префиксы |
| **DataSource** | Наличие, name не пуст, type валиден (Local/External), уникальность имён |
| **DataSet** | Наличие, xsi:type валиден, name не пуст, уникальность, ссылка на dataSource, query не пуст |
| **Fields** | dataPath не пуст, field не пуст, уникальность dataPath в наборе |
| **Links** | source/dest ссылаются на существующие наборы, expressions не пусты |
| **CalcFields** | dataPath не пуст, expression не пуст, уникальность, коллизии с полями наборов |
| **TotalFields** | dataPath не пуст, expression не пуст |
| **Parameters** | name не пуст, уникальность |
| **Templates** | name не пуст, уникальность |
| **GroupTemplates** | template ссылается на существующий template, templateType валиден |
| **Variants** | Наличие, name не пуст, settings element присутствует |
| **Settings** | selection/filter/order ссылаются на известные поля, comparisonType валиден, structure items типизированы |
## Коды выхода
| Код | Значение |
|-----|----------|
| 0 | Ошибок нет (могут быть предупреждения) |
| 1 | Есть ошибки |
## Пример вывода
```
=== Validation: Template.xml ===
[OK] XML parsed successfully
[OK] Root element: DataCompositionSchema
[OK] Default namespace correct
[OK] 1 dataSource(s) found, names unique
[OK] 1 dataSet(s) found, names unique
[OK] DataSet "НаборДанных1": 2 fields, dataPath unique
[OK] 1 totalField(s): dataPath and expression present
[OK] 1 settingsVariant(s) found
=== Result: 0 errors, 0 warnings ===
```
## Верификация
```
/skd-compile <JsonPath> <OutputPath> — генерация XML
/skd-validate <OutputPath> — проверка результата
/skd-info <OutputPath> — визуальная сводка
```
@@ -0,0 +1,717 @@
param(
[Parameter(Mandatory)]
[string]$TemplatePath,
[int]$MaxErrors = 20,
[string]$OutFile
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve path ---
if (-not $TemplatePath.EndsWith(".xml")) {
$candidate = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml"
if (Test-Path $candidate) {
$TemplatePath = $candidate
}
}
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: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)
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 = {
Out-Line ""
Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ==="
$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 = @{}
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()) {
Report-Error "CalculatedField '$path' has empty expression"
$cfOk = $false
}
# 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)) {
Report-Error "Duplicate template name: $tName"
$tplOk = $false
} else {
$tplSeen[$tName] = $true
}
}
if ($tplOk) {
Report-OK "$($templateNodes.Count) template(s): names unique"
}
}
# --- 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","InListByHierarchy",
"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"
}
}
# --- Final output ---
& $finalize
if ($script:errors -gt 0) {
exit 1
}
exit 0