diff --git a/.claude/skills/epf-add-form/scripts/add-form.ps1 b/.claude/skills/epf-add-form/scripts/add-form.ps1
index c8a90b1f..15266f7d 100644
--- a/.claude/skills/epf-add-form/scripts/add-form.ps1
+++ b/.claude/skills/epf-add-form/scripts/add-form.ps1
@@ -1,4 +1,6 @@
-param(
+# epf-add-form v1.0 — Add managed form to 1C processor
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$ProcessorName,
diff --git a/.claude/skills/epf-add-help/scripts/add-help.ps1 b/.claude/skills/epf-add-help/scripts/add-help.ps1
index c1081f39..36ce90f1 100644
--- a/.claude/skills/epf-add-help/scripts/add-help.ps1
+++ b/.claude/skills/epf-add-help/scripts/add-help.ps1
@@ -1,4 +1,6 @@
-param(
+# epf-add-help v1.0 — Add built-in help to 1C processor
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$ProcessorName,
diff --git a/.claude/skills/epf-add-template/SKILL.md b/.claude/skills/epf-add-template/SKILL.md
index 55100184..d005dfc4 100644
--- a/.claude/skills/epf-add-template/SKILL.md
+++ b/.claude/skills/epf-add-template/SKILL.md
@@ -25,7 +25,7 @@ allowed-tools:
|---------------|:------------:|-----------------|--------------------------------------------------|
| ProcessorName | да | — | Имя обработки |
| TemplateName | да | — | Имя макета |
-| TemplateType | да | — | Тип: HTML, Text, SpreadsheetDocument, BinaryData |
+| TemplateType | да | — | Тип: HTML, Text, SpreadsheetDocument, BinaryData, DataCompositionSchema |
| Synonym | нет | = TemplateName | Синоним макета |
| SrcDir | нет | `src` | Каталог исходников |
@@ -45,6 +45,7 @@ pwsh -NoProfile -File .claude/skills/epf-add-template/scripts/add-template.ps1 -
| Text, текстовый документ, текст | TextDocument | `.txt` | Пустой файл |
| SpreadsheetDocument, табличный документ, MXL | SpreadsheetDocument | `.xml` | Минимальный spreadsheet |
| BinaryData, двоичные данные | BinaryData | `.bin` | Пустой файл |
+| DataCompositionSchema, СКД, схема компоновки | DataCompositionSchema | `.xml` | Минимальная DCS-схема |
## Конвенция именования
diff --git a/.claude/skills/epf-add-template/scripts/add-template.ps1 b/.claude/skills/epf-add-template/scripts/add-template.ps1
index a63ac008..ce29c0d5 100644
--- a/.claude/skills/epf-add-template/scripts/add-template.ps1
+++ b/.claude/skills/epf-add-template/scripts/add-template.ps1
@@ -1,4 +1,6 @@
-param(
+# epf-add-template v1.0 — Add template to 1C processor
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$ProcessorName,
@@ -6,7 +8,7 @@
[string]$TemplateName,
[Parameter(Mandatory)]
- [ValidateSet("HTML", "Text", "SpreadsheetDocument", "BinaryData")]
+ [ValidateSet("HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema")]
[string]$TemplateType,
[string]$Synonym = $TemplateName,
@@ -23,6 +25,7 @@ $typeMap = @{
"Text" = @{ TemplateType = "TextDocument"; Ext = ".txt" }
"SpreadsheetDocument" = @{ TemplateType = "SpreadsheetDocument"; Ext = ".xml" }
"BinaryData" = @{ TemplateType = "BinaryData"; Ext = ".bin" }
+ "DataCompositionSchema" = @{ TemplateType = "DataCompositionSchema"; Ext = ".xml" }
}
$tmpl = $typeMap[$TemplateType]
@@ -111,6 +114,25 @@ switch ($TemplateType) {
"BinaryData" {
[System.IO.File]::WriteAllBytes($templateFilePath, @())
}
+ "DataCompositionSchema" {
+ $content = @"
+
+
+
+ ИсточникДанных1
+ Local
+
+
+"@
+ [System.IO.File]::WriteAllText($templateFilePath, $content, $encBom)
+ }
}
# --- 3. Модификация корневого XML ---
diff --git a/.claude/skills/epf-init/scripts/init.ps1 b/.claude/skills/epf-init/scripts/init.ps1
index 68b55d77..76283c42 100644
--- a/.claude/skills/epf-init/scripts/init.ps1
+++ b/.claude/skills/epf-init/scripts/init.ps1
@@ -1,4 +1,6 @@
-param(
+# epf-init v1.0 — Init 1C external data processor scaffold
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$Name,
diff --git a/.claude/skills/epf-remove-form/scripts/remove-form.ps1 b/.claude/skills/epf-remove-form/scripts/remove-form.ps1
index 80c573a8..2fcd5046 100644
--- a/.claude/skills/epf-remove-form/scripts/remove-form.ps1
+++ b/.claude/skills/epf-remove-form/scripts/remove-form.ps1
@@ -1,4 +1,6 @@
-param(
+# epf-remove-form v1.0 — Remove form from 1C processor
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$ProcessorName,
diff --git a/.claude/skills/epf-remove-template/scripts/remove-template.ps1 b/.claude/skills/epf-remove-template/scripts/remove-template.ps1
index c3b4fe52..65945b24 100644
--- a/.claude/skills/epf-remove-template/scripts/remove-template.ps1
+++ b/.claude/skills/epf-remove-template/scripts/remove-template.ps1
@@ -1,4 +1,6 @@
-param(
+# epf-remove-template v1.0 — Remove template from 1C processor
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$ProcessorName,
diff --git a/.claude/skills/form-add/scripts/form-add.ps1 b/.claude/skills/form-add/scripts/form-add.ps1
index 5cdc84c1..0b036d44 100644
--- a/.claude/skills/form-add/scripts/form-add.ps1
+++ b/.claude/skills/form-add/scripts/form-add.ps1
@@ -1,4 +1,6 @@
-param(
+# form-add v1.0 — Add managed form to 1C config object
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$ObjectPath,
diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1
index 496551e0..14a918ba 100644
--- a/.claude/skills/form-compile/scripts/form-compile.ps1
+++ b/.claude/skills/form-compile/scripts/form-compile.ps1
@@ -1,4 +1,6 @@
-param(
+# form-compile v1.0 — Compile 1C managed form from JSON
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$JsonPath,
diff --git a/.claude/skills/form-edit/scripts/form-edit.ps1 b/.claude/skills/form-edit/scripts/form-edit.ps1
index ebc4366f..65add737 100644
--- a/.claude/skills/form-edit/scripts/form-edit.ps1
+++ b/.claude/skills/form-edit/scripts/form-edit.ps1
@@ -1,4 +1,6 @@
-param(
+# form-edit v1.0 — Edit 1C managed form elements
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$FormPath,
diff --git a/.claude/skills/form-info/scripts/form-info.ps1 b/.claude/skills/form-info/scripts/form-info.ps1
index 61ef72da..0604f887 100644
--- a/.claude/skills/form-info/scripts/form-info.ps1
+++ b/.claude/skills/form-info/scripts/form-info.ps1
@@ -1,4 +1,6 @@
-param(
+# form-info v1.0 — Analyze 1C managed form structure
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory=$true)]
[string]$FormPath,
[int]$Limit = 150,
diff --git a/.claude/skills/form-validate/scripts/form-validate.ps1 b/.claude/skills/form-validate/scripts/form-validate.ps1
index 696dd310..33df9299 100644
--- a/.claude/skills/form-validate/scripts/form-validate.ps1
+++ b/.claude/skills/form-validate/scripts/form-validate.ps1
@@ -1,4 +1,6 @@
-param(
+# form-validate v1.0 — Validate 1C managed form
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$FormPath,
diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1
index 5159cd6c..3f80d4c4 100644
--- a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1
+++ b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1
@@ -1,4 +1,6 @@
-param(
+# mxl-compile v1.0 — Compile 1C spreadsheet from JSON
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$JsonPath,
diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1
index 70df68d2..9899981c 100644
--- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1
+++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1
@@ -1,4 +1,6 @@
-param(
+# mxl-decompile v1.0 — Decompile 1C spreadsheet to JSON
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$TemplatePath,
diff --git a/.claude/skills/mxl-info/scripts/mxl-info.ps1 b/.claude/skills/mxl-info/scripts/mxl-info.ps1
index 77327827..d857dd0f 100644
--- a/.claude/skills/mxl-info/scripts/mxl-info.ps1
+++ b/.claude/skills/mxl-info/scripts/mxl-info.ps1
@@ -1,4 +1,6 @@
-param(
+# mxl-info v1.0 — Analyze 1C spreadsheet structure
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[string]$TemplatePath,
[string]$ProcessorName,
[string]$TemplateName,
diff --git a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1
index 2fc6006c..b44a125e 100644
--- a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1
+++ b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1
@@ -1,4 +1,6 @@
-param(
+# mxl-validate v1.0 — Validate 1C spreadsheet
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[string]$TemplatePath,
[string]$ProcessorName,
[string]$TemplateName,
diff --git a/.claude/skills/role-compile/scripts/role-compile.ps1 b/.claude/skills/role-compile/scripts/role-compile.ps1
index 83e08de9..d5332874 100644
--- a/.claude/skills/role-compile/scripts/role-compile.ps1
+++ b/.claude/skills/role-compile/scripts/role-compile.ps1
@@ -1,4 +1,6 @@
-param(
+# role-compile v1.0 — Compile 1C role from JSON
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$JsonPath,
diff --git a/.claude/skills/role-info/scripts/role-info.ps1 b/.claude/skills/role-info/scripts/role-info.ps1
index 9de9c9f1..d127714c 100644
--- a/.claude/skills/role-info/scripts/role-info.ps1
+++ b/.claude/skills/role-info/scripts/role-info.ps1
@@ -1,4 +1,6 @@
-param(
+# role-info v1.0 — Analyze 1C role rights
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory=$true)][string]$RightsPath,
[switch]$ShowDenied,
[int]$Limit = 150,
diff --git a/.claude/skills/role-validate/scripts/role-validate.ps1 b/.claude/skills/role-validate/scripts/role-validate.ps1
index ccad6b59..be8022b5 100644
--- a/.claude/skills/role-validate/scripts/role-validate.ps1
+++ b/.claude/skills/role-validate/scripts/role-validate.ps1
@@ -1,4 +1,6 @@
-param(
+# role-validate v1.0 — Validate 1C role structure
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
[Parameter(Mandatory)]
[string]$RightsPath,
diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md
new file mode 100644
index 00000000..d4d7f191
--- /dev/null
+++ b/.claude/skills/skd-compile/SKILL.md
@@ -0,0 +1,225 @@
+---
+name: skd-compile
+description: Компиляция схемы компоновки данных 1С (СКД) — Template.xml из компактного JSON-определения
+argument-hint:
+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 "" -OutputPath ""
+```
+
+## 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`. Ссылочные типы эмитируются с inline namespace `d5p1:` (`http://v8.1c.ru/8.1/data/enterprise/current-config`). Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией.
+
+**Синонимы типов** (русские и альтернативные): `число` = decimal, `строка` = string, `булево` = boolean, `дата` = date, `датаВремя` = dateTime, `СтандартныйПериод` = StandardPeriod, `СправочникСсылка.X` = CatalogRef.X, `ДокументСсылка.X` = DocumentRef.X, `int`/`number` = decimal, `bool` = boolean. Регистронезависимые.
+
+Роли: `@dimension`, `@account`, `@balance`, `@period`.
+
+Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`.
+
+### Итоги (shorthand)
+
+```json
+"totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"]
+```
+
+### Параметры (shorthand + @autoDates)
+
+```json
+"parameters": [
+ "Период: StandardPeriod = LastMonth @autoDates"
+]
+```
+
+`@autoDates` — автоматически генерирует параметры `ДатаНачала` и `ДатаОкончания` с выражениями `&Период.ДатаНачала` / `&Период.ДатаОкончания` и `availableAsField=false`. Заменяет 5 строк на 1.
+
+### Фильтры — shorthand
+
+```json
+"filter": [
+ "Организация = _ @off @user",
+ "Дата >= 2024-01-01T00:00:00",
+ "Статус filled"
+]
+```
+
+Формат: `"Поле оператор значение @флаги"`. Значение `_` = пустое (placeholder). Флаги: `@off` (use=false), `@user` (userSettingID=auto), `@quickAccess`, `@normal`, `@inaccessible`.
+
+В объектной форме доступны: `viewMode`, `userSettingID`, `userSettingPresentation`.
+
+Группы фильтров (Or/And/Not):
+```json
+{ "group": "Or", "items": [
+ { "group": "And", "items": [
+ { "field": "Статус", "op": "=", "value": "Активен" },
+ { "field": "Сумма", "op": ">", "value": 1000 }
+ ]},
+ { "field": "Количество", "op": "filled" }
+]}
+```
+
+### Параметры данных — shorthand
+
+```json
+"dataParameters": [
+ "Период = LastMonth @user",
+ "Организация @off @user"
+]
+```
+
+Формат: `"Имя [= значение] @флаги"`. Для StandardPeriod варианты (LastMonth, ThisYear и т.д.) распознаются автоматически.
+
+### Структура — string shorthand
+
+```json
+"structure": "Организация > details"
+"structure": "Организация > Номенклатура > details"
+```
+
+`>` разделяет уровни группировки. `details` (или `детали`) = детальные записи. `selection` и `order` по умолчанию `["Auto"]` на каждом уровне.
+
+Для сложных случаев (таблицы, диаграммы, фильтры на уровне группировки) используется объектная форма.
+
+### Варианты настроек
+
+```json
+"settingsVariants": [{
+ "name": "Основной",
+ "settings": {
+ "selection": ["Номенклатура", "Количество", "Auto"],
+ "filter": ["Организация = _ @off @user"],
+ "order": ["Количество desc", "Auto"],
+ "conditionalAppearance": [
+ {
+ "filter": ["Просрочено = true"],
+ "appearance": { "ЦветТекста": "style:ПросроченныеДанныеЦвет" },
+ "presentation": "Выделять просроченные",
+ "viewMode": "Normal",
+ "userSettingID": "auto"
+ }
+ ],
+ "outputParameters": { "Заголовок": "Мой отчёт" },
+ "dataParameters": ["Период = LastMonth @user"],
+ "structure": "Организация > details"
+ }
+}]
+```
+
+### Условное оформление (conditionalAppearance)
+
+```json
+"conditionalAppearance": [
+ {
+ "selection": ["Поле1"],
+ "filter": ["Поле1 notFilled"],
+ "appearance": { "Текст": "Не указано", "ЦветТекста": "style:XXX" },
+ "presentation": "Описание",
+ "viewMode": "Normal",
+ "userSettingID": "auto"
+ }
+]
+```
+
+Типы значений appearance: `style:XXX`/`web:XXX`/`win:XXX` → Color, `true`/`false` → Boolean, параметр `Текст` → LocalStringType, прочее → String.
+
+### Итоги с привязкой к группировкам
+
+```json
+"totalFields": [
+ { "dataPath": "Кол", "expression": "Сумма(Кол)", "group": ["Группа1", "Группа1 Иерархия", "ОбщийИтог"] }
+]
+```
+
+## Примеры
+
+### Минимальный
+
+```json
+{
+ "dataSets": [{
+ "query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура",
+ "fields": ["Наименование"]
+ }]
+}
+```
+
+### С ресурсами, параметрами и @autoDates
+
+```json
+{
+ "dataSets": [{
+ "query": "ВЫБРАТЬ Продажи.Номенклатура, Продажи.Количество, Продажи.Сумма ИЗ РегистрНакопления.Продажи КАК Продажи",
+ "fields": ["Номенклатура: СправочникСсылка.Номенклатура @dimension", "Количество: число(15,3)", "Сумма: число(15,2)"]
+ }],
+ "totalFields": ["Количество: Сумма", "Сумма: Сумма"],
+ "parameters": ["Период: СтандартныйПериод = LastMonth @autoDates"],
+ "settingsVariants": [{
+ "name": "Основной",
+ "settings": {
+ "selection": ["Номенклатура", "Количество", "Сумма", "Auto"],
+ "filter": ["Организация = _ @off @user"],
+ "dataParameters": ["Период = LastMonth @user"],
+ "structure": "Организация > details"
+ }
+ }]
+}
+```
+
+## Верификация
+
+```
+/skd-validate — валидация структуры XML
+/skd-info — визуальная сводка
+/skd-info -Mode variant -Name 1 — проверка варианта настроек
+```
diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1
new file mode 100644
index 00000000..6df434ed
--- /dev/null
+++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1
@@ -0,0 +1,1636 @@
+# skd-compile v1.0 — Compile 1C DCS from JSON
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
+ [Parameter(Mandatory)]
+ [string]$JsonPath,
+
+ [Parameter(Mandatory)]
+ [string]$OutputPath
+)
+
+$ErrorActionPreference = "Stop"
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+# --- 1. Load and validate JSON ---
+
+if (-not (Test-Path $JsonPath)) {
+ Write-Error "File not found: $JsonPath"
+ exit 1
+}
+
+$json = Get-Content -Raw -Encoding UTF8 $JsonPath
+$def = $json | ConvertFrom-Json
+
+if (-not $def.dataSets -or $def.dataSets.Count -eq 0) {
+ Write-Error "JSON must have at least one entry in 'dataSets'"
+ exit 1
+}
+
+# --- 2. XML helpers ---
+
+$script:xml = New-Object System.Text.StringBuilder 16384
+
+function X {
+ param([string]$text)
+ $script:xml.AppendLine($text) | Out-Null
+}
+
+function Esc-Xml {
+ param([string]$s)
+ return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
+}
+
+function Emit-MLText {
+ param([string]$tag, [string]$text, [string]$indent)
+ X "$indent<$tag xsi:type=`"v8:LocalStringType`">"
+ X "$indent`t"
+ X "$indent`t`tru"
+ X "$indent`t`t$(Esc-Xml $text)"
+ X "$indent`t"
+ X "$indent$tag>"
+}
+
+function New-Guid-String {
+ return [System.Guid]::NewGuid().ToString()
+}
+
+# --- 3. Resolve defaults ---
+
+# DataSources
+$dataSources = @()
+if ($def.dataSources) {
+ foreach ($ds in $def.dataSources) {
+ $dataSources += @{
+ name = "$($ds.name)"
+ type = if ($ds.type) { "$($ds.type)" } else { "Local" }
+ }
+ }
+} else {
+ $dataSources += @{ name = "ИсточникДанных1"; type = "Local" }
+}
+
+$defaultSource = $dataSources[0].name
+
+# Auto-name dataSets
+$dsIndex = 1
+foreach ($ds in $def.dataSets) {
+ if (-not $ds.name) {
+ $ds | Add-Member -NotePropertyName "name" -NotePropertyValue "НаборДанных$dsIndex" -Force
+ }
+ $dsIndex++
+}
+
+# --- 4. Type system ---
+
+# Type synonyms — normalize Russian/common names to canonical DSL types
+# Use case-sensitive hashtable to avoid PS 5.1 DuplicateKeyInHashLiteral
+$script:typeSynonyms = New-Object System.Collections.Hashtable
+# Russian names (case doesn't matter — we'll also do case-insensitive lookup)
+$script:typeSynonyms["число"] = "decimal"
+$script:typeSynonyms["строка"] = "string"
+$script:typeSynonyms["булево"] = "boolean"
+$script:typeSynonyms["дата"] = "date"
+$script:typeSynonyms["датавремя"] = "dateTime"
+$script:typeSynonyms["стандартныйпериод"] = "StandardPeriod"
+# English canonical (lowercase for lookup)
+$script:typeSynonyms["bool"] = "boolean"
+$script:typeSynonyms["str"] = "string"
+$script:typeSynonyms["int"] = "decimal"
+$script:typeSynonyms["integer"] = "decimal"
+$script:typeSynonyms["number"] = "decimal"
+$script:typeSynonyms["num"] = "decimal"
+# Reference synonyms (Russian, lowercase)
+$script:typeSynonyms["справочникссылка"] = "CatalogRef"
+$script:typeSynonyms["документссылка"] = "DocumentRef"
+$script:typeSynonyms["перечислениессылка"] = "EnumRef"
+$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef"
+$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef"
+
+function Resolve-TypeStr {
+ param([string]$typeStr)
+ if (-not $typeStr) { return $typeStr }
+
+ # Check for parameterized types: число(15,2), строка(100), etc.
+ if ($typeStr -match '^([^(]+)\((.+)\)$') {
+ $baseName = $Matches[1].Trim()
+ $params = $Matches[2]
+
+ # Resolve base name (case-insensitive via .ToLower())
+ $resolved = $script:typeSynonyms[$baseName.ToLower()]
+ if ($resolved) { return "$resolved($params)" }
+
+ return $typeStr
+ }
+
+ # Check for reference types: СправочникСсылка.Организации → CatalogRef.Организации
+ if ($typeStr.Contains('.')) {
+ $dotIdx = $typeStr.IndexOf('.')
+ $prefix = $typeStr.Substring(0, $dotIdx)
+ $suffix = $typeStr.Substring($dotIdx) # includes the dot
+ $resolved = $script:typeSynonyms[$prefix.ToLower()]
+ if ($resolved) { return "$resolved$suffix" }
+ return $typeStr
+ }
+
+ # Simple name lookup (case-insensitive)
+ $resolved = $script:typeSynonyms[$typeStr.ToLower()]
+ if ($resolved) { return $resolved }
+
+ return $typeStr
+}
+
+function Emit-ValueType {
+ param([string]$typeStr, [string]$indent)
+
+ if (-not $typeStr) { return }
+
+ # Resolve synonyms first
+ $typeStr = Resolve-TypeStr $typeStr
+
+ # boolean
+ if ($typeStr -eq "boolean") {
+ X "$indentxs:boolean"
+ return
+ }
+
+ # string or string(N)
+ if ($typeStr -match '^string(\((\d+)\))?$') {
+ $len = if ($Matches[2]) { $Matches[2] } else { "0" }
+ X "$indentxs:string"
+ X "$indent"
+ X "$indent`t$len"
+ X "$indent`tVariable"
+ X "$indent"
+ return
+ }
+
+ # decimal(D,F) or decimal(D,F,nonneg)
+ if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') {
+ $digits = $Matches[1]
+ $fraction = $Matches[2]
+ $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" }
+ X "$indentxs:decimal"
+ X "$indent"
+ X "$indent`t$digits"
+ X "$indent`t$fraction"
+ X "$indent`t$sign"
+ X "$indent"
+ return
+ }
+
+ # date / dateTime
+ if ($typeStr -match '^(date|dateTime)$') {
+ $fractions = switch ($typeStr) {
+ "date" { "Date" }
+ "dateTime" { "DateTime" }
+ }
+ X "$indentxs:dateTime"
+ X "$indent"
+ X "$indent`t$fractions"
+ X "$indent"
+ return
+ }
+
+ # StandardPeriod
+ if ($typeStr -eq "StandardPeriod") {
+ X "$indentv8:StandardPeriod"
+ return
+ }
+
+ # Reference types: CatalogRef.XXX, DocumentRef.XXX, EnumRef.XXX, etc.
+ # Real DCS files use inline namespace d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config"
+ if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') {
+ X "$indentd5p1:$(Esc-Xml $typeStr)"
+ return
+ }
+
+ # Fallback — assume dot-qualified types are also config references
+ if ($typeStr.Contains('.')) {
+ X "$indentd5p1:$(Esc-Xml $typeStr)"
+ return
+ }
+
+ X "$indent$(Esc-Xml $typeStr)"
+}
+
+# --- 5. Field shorthand parser ---
+
+function Parse-FieldShorthand {
+ param([string]$s)
+
+ $result = @{
+ dataPath = ""; field = ""; title = ""; type = ""
+ roles = @(); restrict = @(); appearance = @{}
+ }
+
+ # Extract @roles
+ $roleMatches = [regex]::Matches($s, '@(\w+)')
+ foreach ($m in $roleMatches) {
+ $result.roles += $m.Groups[1].Value
+ }
+ $s = [regex]::Replace($s, '\s*@\w+', '')
+
+ # Extract #restrictions
+ $restrictMatches = [regex]::Matches($s, '#(\w+)')
+ foreach ($m in $restrictMatches) {
+ $result.restrict += $m.Groups[1].Value
+ }
+ $s = [regex]::Replace($s, '\s*#\w+', '')
+
+ # Split name: type
+ $s = $s.Trim()
+ if ($s.Contains(':')) {
+ $parts = $s -split ':', 2
+ $result.dataPath = $parts[0].Trim()
+ $result.type = Resolve-TypeStr ($parts[1].Trim())
+ } else {
+ $result.dataPath = $s
+ }
+
+ $result.field = $result.dataPath
+ return $result
+}
+
+# --- 6. Total field shorthand parser ---
+
+function Parse-TotalShorthand {
+ param([string]$s)
+
+ # "DataPath: Func" or "DataPath: Func(expr)"
+ $parts = $s -split ':', 2
+ $dataPath = $parts[0].Trim()
+ $funcPart = $parts[1].Trim()
+
+ if ($funcPart -match '^\w+\(') {
+ # Already has expression form: Func(expr)
+ return @{ dataPath = $dataPath; expression = $funcPart }
+ } else {
+ # Short: Func → Func(DataPath)
+ return @{ dataPath = $dataPath; expression = "$funcPart($dataPath)" }
+ }
+}
+
+# --- 7. Parameter shorthand parser ---
+
+function Parse-ParamShorthand {
+ param([string]$s)
+
+ $result = @{ name = ""; type = ""; value = $null; autoDates = $false }
+
+ # Extract @autoDates flag
+ if ($s -match '@autoDates') {
+ $result.autoDates = $true
+ $s = $s -replace '\s*@autoDates', ''
+ }
+
+ # Split "Name: Type = Value"
+ if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.+))?$') {
+ $result.name = $Matches[1].Trim()
+ $result.type = Resolve-TypeStr ($Matches[2].Trim())
+ if ($Matches[4]) {
+ $result.value = $Matches[4].Trim()
+ }
+ } else {
+ $result.name = $s.Trim()
+ }
+
+ return $result
+}
+
+# --- 8. Calculated field shorthand parser ---
+
+function Parse-CalcShorthand {
+ param([string]$s)
+
+ # "DataPath = Expression"
+ $idx = $s.IndexOf('=')
+ if ($idx -gt 0) {
+ return @{
+ dataPath = $s.Substring(0, $idx).Trim()
+ expression = $s.Substring($idx + 1).Trim()
+ }
+ }
+ return @{ dataPath = $s.Trim(); expression = "" }
+}
+
+# --- 8b. DataParameter shorthand parser ---
+# Formats: "Период = LastMonth @user", "Организация @off @user", "Период @user"
+function Parse-DataParamShorthand {
+ param([string]$s)
+
+ $result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null }
+
+ # Extract @flags
+ if ($s -match '@user') {
+ $result.userSettingID = "auto"
+ $s = $s -replace '\s*@user', ''
+ }
+ if ($s -match '@off') {
+ $result.use = $false
+ $s = $s -replace '\s*@off', ''
+ }
+ if ($s -match '@quickAccess') {
+ $result.viewMode = "QuickAccess"
+ $s = $s -replace '\s*@quickAccess', ''
+ }
+ if ($s -match '@normal') {
+ $result.viewMode = "Normal"
+ $s = $s -replace '\s*@normal', ''
+ }
+
+ $s = $s.Trim()
+
+ # Split "Name = Value"
+ if ($s -match '^([^=]+)=\s*(.+)$') {
+ $result.parameter = $Matches[1].Trim()
+ $valStr = $Matches[2].Trim()
+
+ # Detect StandardPeriod variants
+ $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear")
+ if ($periodVariants -contains $valStr) {
+ $result.value = @{ variant = $valStr }
+ } elseif ($valStr -match '^\d{4}-\d{2}-\d{2}T') {
+ $result.value = $valStr
+ } elseif ($valStr -eq "true" -or $valStr -eq "false") {
+ $result.value = [bool]($valStr -eq "true")
+ } else {
+ $result.value = $valStr
+ }
+ } else {
+ $result.parameter = $s
+ }
+
+ return $result
+}
+
+# --- 8c. Filter item shorthand parser ---
+# Formats: "Организация = _ @off @user", "Дата >= 2024-01-01T00:00:00", "Статус filled"
+function Parse-FilterShorthand {
+ param([string]$s)
+
+ $result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null; presentation = $null }
+
+ # Extract @flags
+ if ($s -match '@user') {
+ $result.userSettingID = "auto"
+ $s = $s -replace '\s*@user', ''
+ }
+ if ($s -match '@off') {
+ $result.use = $false
+ $s = $s -replace '\s*@off', ''
+ }
+ if ($s -match '@quickAccess') {
+ $result.viewMode = "QuickAccess"
+ $s = $s -replace '\s*@quickAccess', ''
+ }
+ if ($s -match '@normal') {
+ $result.viewMode = "Normal"
+ $s = $s -replace '\s*@normal', ''
+ }
+ if ($s -match '@inaccessible') {
+ $result.viewMode = "Inaccessible"
+ $s = $s -replace '\s*@inaccessible', ''
+ }
+
+ $s = $s.Trim()
+
+ # Try to match: Field op Value, or Field op (no value for filled/notFilled)
+ # Operators sorted longest first to match >= before >
+ $opPatterns = @('<>', '>=', '<=', '=', '>', '<',
+ 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b',
+ 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b',
+ 'notFilled\b', 'filled\b')
+ $opJoined = $opPatterns -join '|'
+
+ if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") {
+ $result.field = $Matches[1].Trim()
+ $opRaw = $Matches[2].Trim()
+ $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" }
+
+ # Map op
+ $opMap = @{
+ "=" = "Equal"; "<>" = "NotEqual"; ">" = "Greater"; ">=" = "GreaterOrEqual"
+ "<" = "Less"; "<=" = "LessOrEqual"; "in" = "InList"; "notIn" = "NotInList"
+ "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy"
+ "contains" = "Contains"; "notContains" = "NotContains"
+ "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith"
+ "filled" = "Filled"; "notFilled" = "NotFilled"
+ }
+ $mapped = $opMap[$opRaw]
+ if ($mapped) { $result.op = $opRaw } else { $result.op = $opRaw }
+
+ # Parse value (skip "_" which means empty/placeholder)
+ if ($valPart -and $valPart -ne "_") {
+ if ($valPart -eq "true" -or $valPart -eq "false") {
+ $result.value = [bool]($valPart -eq "true")
+ $result["valueType"] = "xs:boolean"
+ } elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') {
+ $result.value = $valPart
+ $result["valueType"] = "xs:dateTime"
+ } elseif ($valPart -match '^\d+(\.\d+)?$') {
+ $result.value = $valPart
+ $result["valueType"] = "xs:decimal"
+ } else {
+ $result.value = $valPart
+ $result["valueType"] = "xs:string"
+ }
+ }
+ } else {
+ # No operator found — just a field name
+ $result.field = $s
+ }
+
+ return $result
+}
+
+# --- 9. Comparison type mapper ---
+
+$script:comparisonTypes = @{
+ "=" = "Equal"; "<>" = "NotEqual"
+ ">" = "Greater"; ">=" = "GreaterOrEqual"
+ "<" = "Less"; "<=" = "LessOrEqual"
+ "in" = "InList"; "notIn" = "NotInList"
+ "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy"
+ "contains" = "Contains"; "notContains" = "NotContains"
+ "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith"
+ "filled" = "Filled"; "notFilled" = "NotFilled"
+}
+
+# --- 10. Output parameter type detection ---
+
+$script:outputParamTypes = @{
+ "Заголовок" = "mltext"
+ "ВыводитьЗаголовок" = "dcsset:DataCompositionTextOutputType"
+ "ВыводитьПараметрыДанных" = "dcsset:DataCompositionTextOutputType"
+ "ВыводитьОтбор" = "dcsset:DataCompositionTextOutputType"
+ "МакетОформления" = "xs:string"
+ "РасположениеПолейГруппировки" = "dcsset:DataCompositionGroupFieldsPlacement"
+ "РасположениеРеквизитов" = "dcsset:DataCompositionAttributesPlacement"
+ "ГоризонтальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement"
+ "ВертикальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement"
+}
+
+# --- 11. Emit sections ---
+
+# === DataSources ===
+function Emit-DataSources {
+ foreach ($ds in $dataSources) {
+ X "`t"
+ X "`t`t$(Esc-Xml $ds.name)"
+ X "`t`t$(Esc-Xml $ds.type)"
+ X "`t"
+ }
+}
+
+# === Fields ===
+function Emit-Field {
+ param($fieldDef, [string]$indent)
+
+ if ($fieldDef -is [string]) {
+ $f = Parse-FieldShorthand $fieldDef
+ } else {
+ $f = @{
+ dataPath = "$($fieldDef.dataPath)"
+ field = if ($fieldDef.field) { "$($fieldDef.field)" } else { "$($fieldDef.dataPath)" }
+ title = if ($fieldDef.title) { "$($fieldDef.title)" } else { "" }
+ type = if ($fieldDef.type) { Resolve-TypeStr "$($fieldDef.type)" } else { "" }
+ roles = @()
+ restrict = @()
+ appearance = @{}
+ }
+ # Parse role
+ if ($fieldDef.role) {
+ if ($fieldDef.role -is [string]) {
+ $f.roles = @($fieldDef.role)
+ } else {
+ # Object form — collect truthy keys
+ $roleObj = $fieldDef.role
+ foreach ($prop in $roleObj.PSObject.Properties) {
+ if ($prop.Value -eq $true) { $f.roles += $prop.Name }
+ }
+ }
+ }
+ # Parse restrictions
+ if ($fieldDef.restrict) {
+ $f.restrict = @($fieldDef.restrict)
+ }
+ # Parse appearance
+ if ($fieldDef.appearance) {
+ foreach ($prop in $fieldDef.appearance.PSObject.Properties) {
+ $f.appearance[$prop.Name] = "$($prop.Value)"
+ }
+ }
+ if ($fieldDef.presentationExpression) {
+ $f["presentationExpression"] = "$($fieldDef.presentationExpression)"
+ }
+ # attrRestrict
+ if ($fieldDef.attrRestrict) {
+ $f["attrRestrict"] = @($fieldDef.attrRestrict)
+ }
+ # role object extras
+ if ($fieldDef.role -and $fieldDef.role -isnot [string]) {
+ $f["roleObj"] = $fieldDef.role
+ }
+ }
+
+ X "$indent"
+ X "$indent`t$(Esc-Xml $f.dataPath)"
+ X "$indent`t$(Esc-Xml $f.field)"
+
+ # Title
+ if ($f.title) {
+ Emit-MLText -tag "title" -text $f.title -indent "$indent`t"
+ }
+
+ # UseRestriction
+ $restrictMap = @{
+ "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition"
+ "noGroup" = "group"; "noOrder" = "order"
+ }
+ if ($f.restrict.Count -gt 0) {
+ X "$indent`t"
+ foreach ($r in $f.restrict) {
+ $xmlName = $restrictMap["$r"]
+ if ($xmlName) {
+ X "$indent`t`t<$xmlName>true$xmlName>"
+ }
+ }
+ X "$indent`t"
+ }
+
+ # AttributeUseRestriction
+ if ($f["attrRestrict"] -and $f["attrRestrict"].Count -gt 0) {
+ X "$indent`t"
+ foreach ($r in $f["attrRestrict"]) {
+ $xmlName = $restrictMap["$r"]
+ if ($xmlName) {
+ X "$indent`t`t<$xmlName>true$xmlName>"
+ }
+ }
+ X "$indent`t"
+ }
+
+ # Role
+ if ($f.roles.Count -gt 0 -or $f["roleObj"]) {
+ X "$indent`t"
+ foreach ($role in $f.roles) {
+ if ($role -eq "period") {
+ # @period -> periodNumber + periodType (not )
+ X "$indent`t`t1"
+ X "$indent`t`tMain"
+ } else {
+ X "$indent`t`ttrue"
+ }
+ }
+ if ($f["roleObj"]) {
+ $ro = $f["roleObj"]
+ if ($ro.accountTypeExpression) {
+ X "$indent`t`t$(Esc-Xml "$($ro.accountTypeExpression)")"
+ }
+ if ($ro.balanceGroup) {
+ X "$indent`t`t$(Esc-Xml "$($ro.balanceGroup)")"
+ }
+ }
+ X "$indent`t"
+ }
+
+ # ValueType
+ if ($f.type) {
+ X "$indent`t"
+ Emit-ValueType -typeStr $f.type -indent "$indent`t`t"
+ X "$indent`t"
+ }
+
+ # Appearance
+ if ($f.appearance -and $f.appearance.Count -gt 0) {
+ X "$indent`t"
+ foreach ($key in $f.appearance.Keys) {
+ $val = $f.appearance[$key]
+ X "$indent`t`t"
+ X "$indent`t`t`t$(Esc-Xml $key)"
+ if ($key -eq "ГоризонтальноеПоложение") {
+ X "$indent`t`t`t$(Esc-Xml $val)"
+ } else {
+ X "$indent`t`t`t$(Esc-Xml $val)"
+ }
+ X "$indent`t`t"
+ }
+ X "$indent`t"
+ }
+
+ # PresentationExpression
+ if ($f["presentationExpression"]) {
+ X "$indent`t$(Esc-Xml $f["presentationExpression"])"
+ }
+
+ X "$indent"
+}
+
+# === DataSets ===
+function Emit-DataSet {
+ param($ds, [string]$indent)
+
+ # Determine type
+ if ($ds.items) {
+ $dsType = "DataSetUnion"
+ } elseif ($ds.objectName) {
+ $dsType = "DataSetObject"
+ } else {
+ $dsType = "DataSetQuery"
+ }
+
+ X "$indent"
+ X "$indent`t$(Esc-Xml "$($ds.name)")"
+
+ # Fields
+ if ($ds.fields) {
+ foreach ($f in $ds.fields) {
+ Emit-Field -fieldDef $f -indent "$indent`t"
+ }
+ }
+
+ # DataSource (not for Union)
+ if ($dsType -ne "DataSetUnion") {
+ $src = if ($ds.source) { "$($ds.source)" } else { $defaultSource }
+ X "$indent`t$(Esc-Xml $src)"
+ }
+
+ # Type-specific content
+ if ($dsType -eq "DataSetQuery") {
+ X "$indent`t$(Esc-Xml "$($ds.query)")"
+ if ($ds.autoFillFields -eq $false) {
+ X "$indent`tfalse"
+ }
+ } elseif ($dsType -eq "DataSetObject") {
+ X "$indent`t$(Esc-Xml "$($ds.objectName)")"
+ } elseif ($dsType -eq "DataSetUnion") {
+ foreach ($item in $ds.items) {
+ # Union items are nested dataSets
+ Emit-DataSet -ds $item -indent "$indent`t" | Out-Null
+ }
+ }
+
+ X "$indent"
+}
+
+function Emit-DataSets {
+ foreach ($ds in $def.dataSets) {
+ Emit-DataSet -ds $ds -indent "`t"
+ }
+}
+
+# === DataSetLinks ===
+function Emit-DataSetLinks {
+ if (-not $def.dataSetLinks) { return }
+ foreach ($link in $def.dataSetLinks) {
+ X "`t"
+ X "`t`t$(Esc-Xml "$($link.source)")"
+ X "`t`t$(Esc-Xml "$($link.dest)")"
+ X "`t`t$(Esc-Xml "$($link.sourceExpr)")"
+ X "`t`t$(Esc-Xml "$($link.destExpr)")"
+ if ($link.parameter) {
+ X "`t`t$(Esc-Xml "$($link.parameter)")"
+ }
+ X "`t"
+ }
+}
+
+# === CalculatedFields ===
+function Emit-CalcFields {
+ if (-not $def.calculatedFields) { return }
+ foreach ($cf in $def.calculatedFields) {
+ if ($cf -is [string]) {
+ $parsed = Parse-CalcShorthand $cf
+ } else {
+ $parsed = @{
+ dataPath = "$($cf.dataPath)"
+ expression = "$($cf.expression)"
+ }
+ }
+
+ X "`t"
+ X "`t`t$(Esc-Xml $parsed.dataPath)"
+ X "`t`t$(Esc-Xml $parsed.expression)"
+
+ if ($cf -isnot [string]) {
+ if ($cf.title) {
+ Emit-MLText -tag "title" -text "$($cf.title)" -indent "`t`t"
+ }
+ if ($cf.type) {
+ $cfType = Resolve-TypeStr "$($cf.type)"
+ X "`t`t"
+ Emit-ValueType -typeStr $cfType -indent "`t`t`t"
+ X "`t`t"
+ }
+ if ($cf.restrict) {
+ $restrictMap = @{
+ "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition"
+ "noGroup" = "group"; "noOrder" = "order"
+ }
+ X "`t`t"
+ foreach ($r in $cf.restrict) {
+ $xmlName = $restrictMap["$r"]
+ if ($xmlName) { X "`t`t`t<$xmlName>true$xmlName>" }
+ }
+ X "`t`t"
+ }
+ if ($cf.appearance) {
+ X "`t`t"
+ foreach ($prop in $cf.appearance.PSObject.Properties) {
+ X "`t`t`t"
+ X "`t`t`t`t$(Esc-Xml $prop.Name)"
+ X "`t`t`t`t$(Esc-Xml "$($prop.Value)")"
+ X "`t`t`t"
+ }
+ X "`t`t"
+ }
+ }
+
+ X "`t"
+ }
+}
+
+# === TotalFields ===
+function Emit-TotalFields {
+ if (-not $def.totalFields) { return }
+ foreach ($tf in $def.totalFields) {
+ if ($tf -is [string]) {
+ $parsed = Parse-TotalShorthand $tf
+ } else {
+ $parsed = @{
+ dataPath = "$($tf.dataPath)"
+ expression = "$($tf.expression)"
+ }
+ if ($tf.group) { $parsed.groups = @($tf.group) }
+ }
+
+ X "`t"
+ X "`t`t$(Esc-Xml $parsed.dataPath)"
+ X "`t`t$(Esc-Xml $parsed.expression)"
+ if ($parsed.groups) {
+ foreach ($g in $parsed.groups) {
+ X "`t`t$(Esc-Xml "$g")"
+ }
+ }
+ X "`t"
+ }
+}
+
+# === Parameters ===
+
+function Emit-SingleParam {
+ param($p, $parsed)
+
+ X "`t"
+ X "`t`t$(Esc-Xml $parsed.name)"
+
+ # Title
+ $title = if ($p -isnot [string] -and $p.title) { "$($p.title)" } else { "" }
+ if ($title) {
+ Emit-MLText -tag "title" -text $title -indent "`t`t"
+ }
+
+ # ValueType
+ if ($parsed.type) {
+ X "`t`t"
+ Emit-ValueType -typeStr $parsed.type -indent "`t`t`t"
+ X "`t`t"
+ }
+
+ # Value
+ Emit-ParamValue -type $parsed.type -val $parsed.value -indent "`t`t"
+
+ # UseRestriction
+ if ($p -isnot [string] -and $p.useRestriction -eq $true) {
+ X "`t`ttrue"
+ }
+
+ # Expression
+ if ($parsed.expression) {
+ X "`t`t$(Esc-Xml $parsed.expression)"
+ }
+
+ # AvailableAsField
+ if ($parsed.availableAsField -eq $false) {
+ X "`t`tfalse"
+ }
+
+ # Use
+ if ($p -isnot [string] -and $p.use) {
+ X "`t`t"
+ }
+
+ X "`t"
+}
+
+function Emit-Parameters {
+ if (-not $def.parameters) { return }
+ foreach ($p in $def.parameters) {
+ if ($p -is [string]) {
+ $parsed = Parse-ParamShorthand $p
+ } else {
+ $parsed = @{
+ name = "$($p.name)"
+ type = if ($p.type) { Resolve-TypeStr "$($p.type)" } else { "" }
+ value = $p.value
+ autoDates = $false
+ }
+ if ($p.expression) { $parsed.expression = "$($p.expression)" }
+ if ($p.availableAsField -eq $false) { $parsed.availableAsField = $false }
+ if ($p.autoDates -eq $true) { $parsed.autoDates = $true }
+ }
+
+ Emit-SingleParam -p $p -parsed $parsed
+
+ # @autoDates: auto-generate ДатаНачала and ДатаОкончания
+ if ($parsed.autoDates) {
+ $paramName = $parsed.name
+ $beginParsed = @{
+ name = "ДатаНачала"; type = "date"; value = $null
+ expression = "&$paramName.ДатаНачала"; availableAsField = $false
+ }
+ Emit-SingleParam -p $null -parsed $beginParsed
+ $endParsed = @{
+ name = "ДатаОкончания"; type = "date"; value = $null
+ expression = "&$paramName.ДатаОкончания"; availableAsField = $false
+ }
+ Emit-SingleParam -p $null -parsed $endParsed
+ }
+ }
+}
+
+function Emit-ParamValue {
+ param([string]$type, $val, [string]$indent)
+
+ if ($null -eq $val) { return }
+
+ $valStr = "$val"
+
+ if ($type -eq "StandardPeriod") {
+ # val is a period variant string like "LastMonth"
+ X "$indent"
+ X "$indent`t$(Esc-Xml $valStr)"
+ X "$indent"
+ } elseif ($type -match '^date') {
+ X "$indent$(Esc-Xml $valStr)"
+ } elseif ($type -eq "boolean") {
+ X "$indent$(Esc-Xml $valStr)"
+ } elseif ($type -match '^decimal') {
+ X "$indent$(Esc-Xml $valStr)"
+ } elseif ($type -match '^string') {
+ X "$indent$(Esc-Xml $valStr)"
+ } else {
+ # Guess from value
+ if ($valStr -match '^\d{4}-\d{2}-\d{2}T') {
+ X "$indent$(Esc-Xml $valStr)"
+ } elseif ($valStr -eq "true" -or $valStr -eq "false") {
+ X "$indent$(Esc-Xml $valStr)"
+ } else {
+ X "$indent$(Esc-Xml $valStr)"
+ }
+ }
+}
+
+# === Templates ===
+function Emit-Templates {
+ if (-not $def.templates) { return }
+ foreach ($t in $def.templates) {
+ X "`t"
+ X "`t`t$(Esc-Xml "$($t.name)")"
+ if ($t.template) {
+ # Raw XML content
+ X "`t`t$($t.template)"
+ }
+ if ($t.parameters) {
+ foreach ($tp in $t.parameters) {
+ X "`t`t"
+ X "`t`t`t$(Esc-Xml "$($tp.name)")"
+ X "`t`t`t$(Esc-Xml "$($tp.expression)")"
+ X "`t`t"
+ }
+ }
+ X "`t"
+ }
+}
+
+# === GroupTemplates ===
+function Emit-GroupTemplates {
+ if (-not $def.groupTemplates) { return }
+ foreach ($gt in $def.groupTemplates) {
+ X "`t"
+ X "`t`t$(Esc-Xml "$($gt.groupField)")"
+ X "`t`t$(Esc-Xml "$($gt.templateType)")"
+ X "`t`t$(Esc-Xml "$($gt.template)")"
+ X "`t"
+ }
+}
+
+# === Settings Variants ===
+
+function Emit-Selection {
+ param($items, [string]$indent, [switch]$skipAuto)
+
+ if (-not $items -or $items.Count -eq 0) { return }
+
+ X "$indent"
+ foreach ($item in $items) {
+ if ($item -is [string]) {
+ if ($item -eq "Auto") {
+ if (-not $skipAuto) {
+ X "$indent`t"
+ }
+ } else {
+ X "$indent`t"
+ X "$indent`t`t$(Esc-Xml $item)"
+ X "$indent`t"
+ }
+ } else {
+ X "$indent`t"
+ X "$indent`t`t$(Esc-Xml "$($item.field)")"
+ if ($item.title) {
+ X "$indent`t`t"
+ X "$indent`t`t`t"
+ X "$indent`t`t`t`tru"
+ X "$indent`t`t`t`t$(Esc-Xml "$($item.title)")"
+ X "$indent`t`t`t"
+ X "$indent`t`t"
+ }
+ X "$indent`t"
+ }
+ }
+ X "$indent"
+}
+
+function Emit-FilterItem {
+ param($item, [string]$indent)
+
+ if ($item.group) {
+ # FilterItemGroup
+ $groupType = switch ("$($item.group)") {
+ "And" { "AndGroup" }
+ "Or" { "OrGroup" }
+ "Not" { "NotGroup" }
+ default { "$($item.group)Group" }
+ }
+ X "$indent"
+ X "$indent`t$groupType"
+ if ($item.items) {
+ foreach ($sub in $item.items) {
+ Emit-FilterItem -item $sub -indent "$indent`t"
+ }
+ }
+ X "$indent"
+ return
+ }
+
+ # FilterItemComparison
+ X "$indent"
+
+ if ($item.use -eq $false) {
+ X "$indent`tfalse"
+ }
+
+ X "$indent`t$(Esc-Xml "$($item.field)")"
+
+ $compType = $script:comparisonTypes["$($item.op)"]
+ if (-not $compType) { $compType = "$($item.op)" }
+ X "$indent`t$(Esc-Xml $compType)"
+
+ # Right value
+ if ($null -ne $item.value) {
+ $vt = if ($item.valueType) { "$($item.valueType)" } else { "" }
+ if (-not $vt) {
+ $v = $item.value
+ if ($v -is [bool]) {
+ $vt = "xs:boolean"
+ } elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) {
+ $vt = "xs:decimal"
+ } elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') {
+ $vt = "xs:dateTime"
+ } else {
+ $vt = "xs:string"
+ }
+ }
+ $vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" }
+ X "$indent`t$vStr"
+ }
+
+ if ($item.presentation) {
+ X "$indent`t"
+ X "$indent`t`t"
+ X "$indent`t`t`tru"
+ X "$indent`t`t`t$(Esc-Xml "$($item.presentation)")"
+ X "$indent`t`t"
+ X "$indent`t"
+ }
+
+ if ($item.viewMode) {
+ X "$indent`t$(Esc-Xml "$($item.viewMode)")"
+ }
+
+ if ($item.userSettingID) {
+ $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
+ X "$indent`t$(Esc-Xml $uid)"
+ }
+
+ if ($item.userSettingPresentation) {
+ X "$indent`t"
+ X "$indent`t`t"
+ X "$indent`t`t`tru"
+ X "$indent`t`t`t$(Esc-Xml "$($item.userSettingPresentation)")"
+ X "$indent`t`t"
+ X "$indent`t"
+ }
+
+ X "$indent"
+}
+
+function Emit-Filter {
+ param($items, [string]$indent)
+
+ if (-not $items -or $items.Count -eq 0) { return }
+
+ X "$indent"
+ foreach ($item in $items) {
+ if ($item -is [string]) {
+ # Parse shorthand: "Организация = _ @off @user"
+ $parsed = Parse-FilterShorthand $item
+ $filterObj = New-Object PSObject
+ $filterObj | Add-Member -NotePropertyName "field" -NotePropertyValue $parsed.field
+ $filterObj | Add-Member -NotePropertyName "op" -NotePropertyValue $parsed.op
+ if ($parsed.use -eq $false) {
+ $filterObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false
+ }
+ if ($null -ne $parsed.value) {
+ $filterObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value
+ }
+ if ($parsed["valueType"]) {
+ $filterObj | Add-Member -NotePropertyName "valueType" -NotePropertyValue $parsed["valueType"]
+ }
+ if ($parsed.userSettingID) {
+ $filterObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID
+ }
+ if ($parsed.viewMode) {
+ $filterObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode
+ }
+ Emit-FilterItem -item $filterObj -indent "$indent`t"
+ } else {
+ Emit-FilterItem -item $item -indent "$indent`t"
+ }
+ }
+ X "$indent"
+}
+
+function Emit-Order {
+ param($items, [string]$indent, [switch]$skipAuto)
+
+ if (-not $items -or $items.Count -eq 0) { return }
+
+ X "$indent"
+ foreach ($item in $items) {
+ if ($item -is [string]) {
+ if ($item -eq "Auto") {
+ if (-not $skipAuto) {
+ X "$indent`t"
+ }
+ } else {
+ $parts = $item -split '\s+'
+ $field = $parts[0]
+ $dir = "Asc"
+ if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)desc$') { $dir = "Desc" }
+ elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)asc$') { $dir = "Asc" }
+ X "$indent`t"
+ X "$indent`t`t$(Esc-Xml $field)"
+ X "$indent`t`t$dir"
+ X "$indent`t"
+ }
+ }
+ }
+ X "$indent"
+}
+
+function Emit-AppearanceValue {
+ param([string]$key, $val, [string]$indent)
+
+ X "$indent"
+ if ($val -is [PSCustomObject] -and $val.use -ne $null -and $val.use -eq $false) {
+ X "$indent`tfalse"
+ X "$indent`t$(Esc-Xml $key)"
+ $actualVal = "$($val.value)"
+ } else {
+ X "$indent`t$(Esc-Xml $key)"
+ $actualVal = "$val"
+ }
+
+ # Auto-detect value type
+ if ($actualVal -match '^(style|web|win):') {
+ X "$indent`t$(Esc-Xml $actualVal)"
+ } elseif ($actualVal -eq "true" -or $actualVal -eq "false") {
+ X "$indent`t$actualVal"
+ } elseif ($key -eq "Текст" -or $key -eq "Заголовок") {
+ X "$indent`t"
+ X "$indent`t`t"
+ X "$indent`t`t`tru"
+ X "$indent`t`t`t$(Esc-Xml $actualVal)"
+ X "$indent`t`t"
+ X "$indent`t"
+ } else {
+ X "$indent`t$(Esc-Xml $actualVal)"
+ }
+ X "$indent"
+}
+
+function Emit-ConditionalAppearance {
+ param($items, [string]$indent)
+
+ if (-not $items -or $items.Count -eq 0) { return }
+
+ X "$indent"
+ foreach ($ca in $items) {
+ X "$indent`t"
+
+ # Selection (which fields to apply to; empty = all)
+ if ($ca.selection -and $ca.selection.Count -gt 0) {
+ X "$indent`t`t"
+ foreach ($sel in $ca.selection) {
+ X "$indent`t`t`t"
+ X "$indent`t`t`t`t$(Esc-Xml "$sel")"
+ X "$indent`t`t`t"
+ }
+ X "$indent`t`t"
+ } else {
+ X "$indent`t`t"
+ }
+
+ # Filter (reuse existing Emit-Filter logic)
+ if ($ca.filter) {
+ Emit-Filter -items $ca.filter -indent "$indent`t`t"
+ }
+
+ # Appearance (parameter-value pairs)
+ if ($ca.appearance) {
+ X "$indent`t`t"
+ foreach ($prop in $ca.appearance.PSObject.Properties) {
+ Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$indent`t`t`t"
+ }
+ X "$indent`t`t"
+ }
+
+ # Presentation
+ if ($ca.presentation) {
+ X "$indent`t`t$(Esc-Xml "$($ca.presentation)")"
+ }
+
+ # ViewMode
+ if ($ca.viewMode) {
+ X "$indent`t`t$(Esc-Xml "$($ca.viewMode)")"
+ }
+
+ # UserSettingID
+ if ($ca.userSettingID) {
+ $uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" }
+ X "$indent`t`t$(Esc-Xml $uid)"
+ }
+
+ X "$indent`t"
+ }
+ X "$indent"
+}
+
+function Emit-OutputParameters {
+ param($params, [string]$indent)
+
+ if (-not $params) { return }
+
+ X "$indent"
+ foreach ($prop in $params.PSObject.Properties) {
+ $key = $prop.Name
+ $val = "$($prop.Value)"
+ $ptype = $script:outputParamTypes[$key]
+ if (-not $ptype) { $ptype = "xs:string" }
+
+ X "$indent`t"
+ X "$indent`t`t$(Esc-Xml $key)"
+ if ($ptype -eq "mltext") {
+ X "$indent`t`t"
+ X "$indent`t`t`t"
+ X "$indent`t`t`t`tru"
+ X "$indent`t`t`t`t$(Esc-Xml $val)"
+ X "$indent`t`t`t"
+ X "$indent`t`t"
+ } else {
+ X "$indent`t`t$(Esc-Xml $val)"
+ }
+ X "$indent`t"
+ }
+ X "$indent"
+}
+
+function Emit-DataParameters {
+ param($items, [string]$indent)
+
+ if (-not $items -or $items.Count -eq 0) { return }
+
+ X "$indent"
+ foreach ($dp in $items) {
+ # Support string shorthand
+ if ($dp -is [string]) {
+ $parsed = Parse-DataParamShorthand $dp
+ $dpObj = New-Object PSObject
+ $dpObj | Add-Member -NotePropertyName "parameter" -NotePropertyValue $parsed.parameter
+ if ($null -ne $parsed.value) {
+ $dpObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value
+ }
+ if ($parsed.use -eq $false) {
+ $dpObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false
+ }
+ if ($parsed.userSettingID) {
+ $dpObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID
+ }
+ if ($parsed.viewMode) {
+ $dpObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode
+ }
+ $dp = $dpObj
+ }
+
+ X "$indent`t"
+
+ if ($dp.use -eq $false) {
+ X "$indent`t`tfalse"
+ }
+
+ X "$indent`t`t$(Esc-Xml "$($dp.parameter)")"
+
+ # Value
+ if ($null -ne $dp.value) {
+ if ($dp.value -is [PSCustomObject] -and $dp.value.variant) {
+ # StandardPeriod (object form from JSON)
+ X "$indent`t`t"
+ X "$indent`t`t`t$(Esc-Xml "$($dp.value.variant)")"
+ X "$indent`t`t"
+ } elseif ($dp.value -is [hashtable] -and $dp.value.variant) {
+ # StandardPeriod (hashtable from shorthand parser)
+ X "$indent`t`t"
+ X "$indent`t`t`t$(Esc-Xml "$($dp.value.variant)")"
+ X "$indent`t`t"
+ } elseif ($dp.value -is [bool]) {
+ $bv = "$($dp.value)".ToLower()
+ X "$indent`t`t$(Esc-Xml $bv)"
+ } elseif ("$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') {
+ X "$indent`t`t$(Esc-Xml "$($dp.value)")"
+ } else {
+ X "$indent`t`t$(Esc-Xml "$($dp.value)")"
+ }
+ }
+
+ if ($dp.viewMode) {
+ X "$indent`t`t$(Esc-Xml "$($dp.viewMode)")"
+ }
+
+ if ($dp.userSettingID) {
+ $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" }
+ X "$indent`t`t$(Esc-Xml $uid)"
+ }
+
+ if ($dp.userSettingPresentation) {
+ X "$indent`t`t"
+ X "$indent`t`t`t"
+ X "$indent`t`t`t`tru"
+ X "$indent`t`t`t`t$(Esc-Xml "$($dp.userSettingPresentation)")"
+ X "$indent`t`t`t"
+ X "$indent`t`t"
+ }
+
+ X "$indent`t"
+ }
+ X "$indent"
+}
+
+# === Structure items (recursive) ===
+
+function Emit-GroupItems {
+ param($groupBy, [string]$indent)
+
+ if (-not $groupBy -or $groupBy.Count -eq 0) { return }
+
+ X "$indent"
+ foreach ($field in $groupBy) {
+ if ($field -is [string]) {
+ X "$indent`t"
+ X "$indent`t`t$(Esc-Xml $field)"
+ X "$indent`t`tItems"
+ X "$indent`t`tNone"
+ X "$indent`t`t0001-01-01T00:00:00"
+ X "$indent`t`t0001-01-01T00:00:00"
+ X "$indent`t"
+ } else {
+ # Object form
+ X "$indent`t"
+ X "$indent`t`t$(Esc-Xml "$($field.field)")"
+ $gt = if ($field.groupType) { "$($field.groupType)" } else { "Items" }
+ X "$indent`t`t$(Esc-Xml $gt)"
+ $pat = if ($field.periodAdditionType) { "$($field.periodAdditionType)" } else { "None" }
+ X "$indent`t`t$(Esc-Xml $pat)"
+ X "$indent`t`t0001-01-01T00:00:00"
+ X "$indent`t`t0001-01-01T00:00:00"
+ X "$indent`t"
+ }
+ }
+ X "$indent"
+}
+
+# Parse structure string shorthand: "Организация > Номенклатура > details"
+function Parse-StructureShorthand {
+ param([string]$s)
+
+ $segments = $s -split '\s*>\s*'
+ $result = @()
+
+ # Build nested groups from right to left
+ $innermost = $null
+ for ($i = $segments.Count - 1; $i -ge 0; $i--) {
+ $seg = $segments[$i].Trim()
+ $group = New-Object PSObject
+ $group | Add-Member -NotePropertyName "type" -NotePropertyValue "group"
+
+ if ($seg -match '^(?i)(details|детали)$') {
+ # Empty groupBy = detailed records
+ $group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @()
+ } else {
+ $group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @($seg)
+ }
+
+ if ($null -ne $innermost) {
+ $group | Add-Member -NotePropertyName "children" -NotePropertyValue @($innermost)
+ }
+ $innermost = $group
+ }
+
+ if ($innermost) { $result += $innermost }
+ return ,$result
+}
+
+function Emit-StructureItem {
+ param($item, [string]$indent)
+
+ $type = "$($item.type)"
+
+ if ($type -eq "group") {
+ X "$indent"
+
+ if ($item.name) {
+ X "$indent`t$(Esc-Xml "$($item.name)")"
+ }
+
+ Emit-GroupItems -groupBy $item.groupBy -indent "$indent`t"
+
+ # Default order to ["Auto"] if not specified
+ $orderItems = $item.order
+ if (-not $orderItems) { $orderItems = @("Auto") }
+ Emit-Order -items $orderItems -indent "$indent`t"
+
+ # Default selection to ["Auto"] if not specified
+ $selItems = $item.selection
+ if (-not $selItems) { $selItems = @("Auto") }
+ Emit-Selection -items $selItems -indent "$indent`t"
+
+ Emit-Filter -items $item.filter -indent "$indent`t"
+
+ if ($item.outputParameters) {
+ Emit-OutputParameters -params $item.outputParameters -indent "$indent`t"
+ }
+
+ # Nested children
+ if ($item.children) {
+ foreach ($child in $item.children) {
+ Emit-StructureItem -item $child -indent "$indent`t"
+ }
+ }
+
+ X "$indent"
+ }
+ elseif ($type -eq "table") {
+ X "$indent"
+
+ if ($item.name) {
+ X "$indent`t$(Esc-Xml "$($item.name)")"
+ }
+
+ # Columns
+ if ($item.columns) {
+ foreach ($col in $item.columns) {
+ X "$indent`t"
+ Emit-GroupItems -groupBy $col.groupBy -indent "$indent`t`t"
+ $colOrder = $col.order; if (-not $colOrder) { $colOrder = @("Auto") }
+ Emit-Order -items $colOrder -indent "$indent`t`t"
+ $colSel = $col.selection; if (-not $colSel) { $colSel = @("Auto") }
+ Emit-Selection -items $colSel -indent "$indent`t`t"
+ X "$indent`t"
+ }
+ }
+
+ # Rows
+ if ($item.rows) {
+ foreach ($row in $item.rows) {
+ X "$indent`t"
+ if ($row.name) {
+ X "$indent`t`t$(Esc-Xml "$($row.name)")"
+ }
+ Emit-GroupItems -groupBy $row.groupBy -indent "$indent`t`t"
+ $rowOrder = $row.order; if (-not $rowOrder) { $rowOrder = @("Auto") }
+ Emit-Order -items $rowOrder -indent "$indent`t`t"
+ $rowSel = $row.selection; if (-not $rowSel) { $rowSel = @("Auto") }
+ Emit-Selection -items $rowSel -indent "$indent`t`t"
+ X "$indent`t"
+ }
+ }
+
+ X "$indent"
+ }
+ elseif ($type -eq "chart") {
+ X "$indent"
+
+ if ($item.name) {
+ X "$indent`t$(Esc-Xml "$($item.name)")"
+ }
+
+ # Points
+ if ($item.points) {
+ X "$indent`t"
+ Emit-GroupItems -groupBy $item.points.groupBy -indent "$indent`t`t"
+ $ptOrder = $item.points.order; if (-not $ptOrder) { $ptOrder = @("Auto") }
+ Emit-Order -items $ptOrder -indent "$indent`t`t"
+ $ptSel = $item.points.selection; if (-not $ptSel) { $ptSel = @("Auto") }
+ Emit-Selection -items $ptSel -indent "$indent`t`t"
+ X "$indent`t"
+ }
+
+ # Series
+ if ($item.series) {
+ X "$indent`t"
+ Emit-GroupItems -groupBy $item.series.groupBy -indent "$indent`t`t"
+ $srOrder = $item.series.order; if (-not $srOrder) { $srOrder = @("Auto") }
+ Emit-Order -items $srOrder -indent "$indent`t`t"
+ $srSel = $item.series.selection; if (-not $srSel) { $srSel = @("Auto") }
+ Emit-Selection -items $srSel -indent "$indent`t`t"
+ X "$indent`t"
+ }
+
+ # Selection (chart values)
+ Emit-Selection -items $item.selection -indent "$indent`t"
+
+ if ($item.outputParameters) {
+ Emit-OutputParameters -params $item.outputParameters -indent "$indent`t"
+ }
+
+ X "$indent"
+ }
+}
+
+function Emit-SettingsVariants {
+ $variants = $def.settingsVariants
+
+ # Default variant if none specified
+ if (-not $variants -or $variants.Count -eq 0) {
+ $variants = @(@{
+ name = "Основной"
+ presentation = "Основной"
+ settings = @{
+ selection = @("Auto")
+ structure = @(@{
+ type = "group"
+ order = @("Auto")
+ selection = @("Auto")
+ })
+ }
+ })
+ # Convert to PSCustomObject-like structure
+ $variants = @($variants | ForEach-Object {
+ $v = New-Object PSObject
+ $v | Add-Member -NotePropertyName "name" -NotePropertyValue $_.name
+ $v | Add-Member -NotePropertyName "presentation" -NotePropertyValue $_.presentation
+ $settingsObj = New-Object PSObject
+ $settingsObj | Add-Member -NotePropertyName "selection" -NotePropertyValue $_.settings.selection
+ $structItem = New-Object PSObject
+ $structItem | Add-Member -NotePropertyName "type" -NotePropertyValue "group"
+ $structItem | Add-Member -NotePropertyName "order" -NotePropertyValue @("Auto")
+ $structItem | Add-Member -NotePropertyName "selection" -NotePropertyValue @("Auto")
+ $settingsObj | Add-Member -NotePropertyName "structure" -NotePropertyValue @($structItem)
+ $v | Add-Member -NotePropertyName "settings" -NotePropertyValue $settingsObj
+ $v
+ })
+ }
+
+ foreach ($v in $variants) {
+ X "`t"
+ X "`t`t$(Esc-Xml "$($v.name)")"
+
+ $pres = if ($v.presentation) { "$($v.presentation)" } else { "$($v.name)" }
+ X "`t`t"
+ X "`t`t`t"
+ X "`t`t`t`tru"
+ X "`t`t`t`t$(Esc-Xml $pres)"
+ X "`t`t`t"
+ X "`t`t"
+
+ X "`t`t"
+
+ $s = $v.settings
+
+ # Selection (Auto items only belong at group level, not top-level settings)
+ if ($s.selection) {
+ Emit-Selection -items $s.selection -indent "`t`t`t" -skipAuto
+ }
+
+ # Filter
+ if ($s.filter) {
+ Emit-Filter -items $s.filter -indent "`t`t`t"
+ }
+
+ # Order (Auto items only belong at group level, not top-level settings)
+ if ($s.order) {
+ Emit-Order -items $s.order -indent "`t`t`t" -skipAuto
+ }
+
+ # ConditionalAppearance
+ if ($s.conditionalAppearance) {
+ Emit-ConditionalAppearance -items $s.conditionalAppearance -indent "`t`t`t"
+ }
+
+ # OutputParameters
+ if ($s.outputParameters) {
+ Emit-OutputParameters -params $s.outputParameters -indent "`t`t`t"
+ }
+
+ # DataParameters
+ if ($s.dataParameters) {
+ Emit-DataParameters -items $s.dataParameters -indent "`t`t`t"
+ }
+
+ # Structure (supports string shorthand: "Организация > details")
+ if ($s.structure) {
+ $structItems = $s.structure
+ if ($structItems -is [string]) {
+ $structItems = Parse-StructureShorthand $structItems
+ }
+ foreach ($item in $structItems) {
+ Emit-StructureItem -item $item -indent "`t`t`t"
+ }
+ }
+
+ X "`t`t"
+ X "`t"
+ }
+}
+
+# --- 12. Assemble XML ---
+
+X ""
+X ""
+
+Emit-DataSources
+Emit-DataSets
+Emit-DataSetLinks
+Emit-CalcFields
+Emit-TotalFields
+Emit-Parameters
+Emit-Templates
+Emit-GroupTemplates
+Emit-SettingsVariants
+
+X ''
+
+# --- 13. Write output ---
+
+$parentDir = [System.IO.Path]::GetDirectoryName($OutputPath)
+if ($parentDir -and -not (Test-Path $parentDir)) {
+ New-Item -ItemType Directory -Force $parentDir | Out-Null
+}
+
+$content = $script:xml.ToString()
+$utf8Bom = New-Object System.Text.UTF8Encoding $true
+[System.IO.File]::WriteAllText($OutputPath, $content, $utf8Bom)
+
+# --- 14. Statistics ---
+
+$dsCount = $def.dataSets.Count
+$fieldCount = 0
+foreach ($ds in $def.dataSets) {
+ if ($ds.fields) { $fieldCount += $ds.fields.Count }
+}
+$calcCount = if ($def.calculatedFields) { $def.calculatedFields.Count } else { 0 }
+$totalCount = if ($def.totalFields) { $def.totalFields.Count } else { 0 }
+$paramCount = if ($def.parameters) { $def.parameters.Count } else { 0 }
+$variantCount = if ($def.settingsVariants) { $def.settingsVariants.Count } else { 1 }
+$fileSize = (Get-Item $OutputPath).Length
+
+Write-Host "OK $OutputPath"
+Write-Host " DataSets: $dsCount Fields: $fieldCount Calculated: $calcCount Totals: $totalCount Params: $paramCount Variants: $variantCount"
+Write-Host " Size: $fileSize bytes"
diff --git a/.claude/skills/skd-edit/SKILL.md b/.claude/skills/skd-edit/SKILL.md
new file mode 100644
index 00000000..26eb1d2a
--- /dev/null
+++ b/.claude/skills/skd-edit/SKILL.md
@@ -0,0 +1,217 @@
+---
+name: skd-edit
+description: Точечное редактирование схемы компоновки данных 1С (СКД) — добавление/удаление полей, итогов, фильтров, параметров, вычисляемых полей
+argument-hint: -Operation -Value
+allowed-tools:
+ - Bash
+ - Read
+ - Write
+ - Glob
+---
+
+# /skd-edit — точечное редактирование СКД (Template.xml)
+
+Атомарные операции модификации существующей схемы компоновки данных: добавление, удаление и модификация полей, итогов, фильтров, параметров, настроек варианта, управление структурой, замена запроса.
+
+## Параметры и команда
+
+| Параметр | Описание |
+|----------|----------|
+| `TemplatePath` | Путь к Template.xml (или к папке — автодополнение Ext/Template.xml) |
+| `Operation` | Операция (см. список ниже) |
+| `Value` | Значение операции (shorthand-строка или текст запроса) |
+| `DataSet` | (опц.) Имя набора данных (умолч. первый) |
+| `Variant` | (опц.) Имя варианта настроек (умолч. первый) |
+| `NoSelection` | (опц.) Не добавлять поле в selection варианта |
+
+```powershell
+powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 -TemplatePath "" -Operation -Value ""
+```
+
+## Пакетный режим (batch)
+
+Несколько значений в одном вызове через разделитель `;;`:
+
+```powershell
+-Operation add-field -Value "Цена: decimal(15,2) ;; Количество: decimal(15,3) ;; Сумма: decimal(15,2)"
+```
+
+Работает для всех операций кроме `set-query`, `set-structure` и `add-dataSet`.
+
+## Операции
+
+### add-field — добавить поле в набор данных
+
+Shorthand: `"Имя [Заголовок]: тип @роль #ограничение"`.
+
+```
+"Цена: decimal(15,2)"
+"Организация [Орг-ция]: CatalogRef.Организации @dimension"
+"Служебное: string #noFilter #noOrder"
+```
+
+Поле добавляется в набор и в selection варианта (если нет `-NoSelection`). Дубликат dataPath — предупреждение, пропуск.
+
+### add-total — добавить итог
+
+```
+"Цена: Среднее"
+"Стоимость: Сумма(Кол * Цена)"
+```
+
+### add-calculated-field — добавить вычисляемое поле
+
+Shorthand: `"Имя [Заголовок]: тип = Выражение"`.
+
+```
+"Маржа = Продажа - Закупка"
+"Наценка [Наценка, %]: decimal(10,2) = Маржа / Закупка * 100"
+```
+
+Также добавляется в selection варианта.
+
+### add-parameter — добавить параметр
+
+```
+"Период: StandardPeriod = LastMonth @autoDates"
+"Организация: CatalogRef.Организации"
+```
+
+`@autoDates` генерирует `ДатаНачала` и `ДатаОкончания` автоматически.
+
+### add-filter — добавить фильтр в вариант
+
+Shorthand: `"Поле оператор значение @флаги"`. Флаги: `@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`.
+
+```
+"Номенклатура = _ @off @user"
+"Дата >= 2024-01-01T00:00:00"
+"Статус filled"
+```
+
+### add-dataParameter — добавить параметр данных в вариант
+
+Shorthand: `"Имя [= значение] @флаги"`.
+
+```
+"Период = LastMonth @user"
+"Организация @off @user"
+```
+
+### add-order — добавить сортировку
+
+Shorthand: `"Поле [desc]"`. По умолчанию asc. `Auto` — авто-элемент.
+
+```
+"Количество desc"
+"Auto"
+```
+
+### add-selection — добавить элемент выборки
+
+```
+"Номенклатура"
+"Auto"
+```
+
+### add-dataSetLink — добавить связь наборов данных
+
+Shorthand: `"Источник > Приёмник on ВырИсточника = ВырПриёмника [param Имя]"`.
+
+```
+"Набор1 > Набор2 on Поле1 = Поле2"
+"Набор1 > Набор2 on Поле1 = Поле2 [param Связь]"
+```
+
+### add-dataSet — добавить набор данных
+
+Shorthand: `"Имя: ТЕКСТ_ЗАПРОСА"` или `"ТЕКСТ_ЗАПРОСА"` (авто-имя `НаборДанныхN`).
+
+```
+"Доп: ВЫБРАТЬ 1 КАК Тест"
+"ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура"
+```
+
+`dataSource` берётся из первого существующего. Дубликат имени — предупреждение, пропуск. Не поддерживает пакетный режим (запрос может содержать `;;`).
+
+### add-variant — добавить вариант настроек
+
+Shorthand: `"Имя [Представление]"`. Представление опционально, по умолчанию = имя.
+
+```
+"Детальный"
+"Детальный [Детальный отчёт]"
+```
+
+Создаёт вариант с Auto selection + detail group. Дубликат имени — предупреждение, пропуск.
+
+### add-conditionalAppearance — добавить условное оформление
+
+Shorthand: `"Параметр = значение [when условие] [for Поле1, Поле2]"`. Блок `when` — синтаксис `add-filter` (Поле оператор значение).
+
+```
+"ЦветТекста = web:Red when Сумма < 0"
+"ЦветФона = web:LightGreen when Статус = Одобрен for Статус"
+"МинимальнаяШирина = 50 for Организация"
+"Формат = ЧДЦ=2 for Цена, Сумма"
+```
+
+Типы значений (автодетект): `web:*`/`style:*`/`win:*` → цвет, `true`/`false` → boolean, иначе строка.
+
+### set-query — заменить текст запроса
+
+Не поддерживает пакетный режим. Value — полный текст запроса.
+
+### set-outputParameter — установить параметр вывода
+
+```
+"Заголовок = Мой отчёт"
+"ВыводитьЗаголовок = true"
+```
+
+Если параметр уже существует — заменяет значение.
+
+### set-structure — установить структуру варианта
+
+Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — детальные записи. Заменяет всю структуру. Не поддерживает пакетный режим.
+
+```
+"Организация > Номенклатура > details"
+"details"
+```
+
+### modify-field — изменить существующее поле
+
+Тот же shorthand что и `add-field`. Находит по dataPath, объединяет свойства (непустые переопределяют), сохраняет позицию.
+
+```
+"Цена [Цена USD]: decimal(10,4) @dimension"
+```
+
+### modify-filter — изменить существующий фильтр
+
+Тот же shorthand что и `add-filter`. Находит по полю, обновляет оператор/значение/флаги.
+
+### modify-dataParameter — изменить параметр данных
+
+Тот же shorthand что и `add-dataParameter`. Находит по имени, обновляет значение/флаги.
+
+### remove-* и clear-*
+
+| Операция | Value | Действие |
+|----------|-------|----------|
+| `remove-field` | dataPath | Удаляет поле из набора + из selection варианта |
+| `remove-total` | dataPath | Удаляет итог |
+| `remove-calculated-field` | dataPath | Удаляет вычисляемое поле + из selection |
+| `remove-parameter` | name | Удаляет параметр |
+| `remove-filter` | поле | Удаляет первый фильтр с указанным полем |
+| `clear-selection` | `*` | Очищает все элементы selection |
+| `clear-order` | `*` | Очищает все элементы order |
+| `clear-filter` | `*` | Очищает все элементы filter |
+
+## Верификация
+
+```
+/skd-validate — валидация структуры после редактирования
+/skd-info — визуальная сводка
+```
diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1
new file mode 100644
index 00000000..6d92c3e3
--- /dev/null
+++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1
@@ -0,0 +1,2291 @@
+# skd-edit v1.1 — Atomic 1C DCS editor
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
+ [Parameter(Mandatory)]
+ [string]$TemplatePath,
+
+ [Parameter(Mandatory)]
+ [ValidateSet(
+ "add-field","add-total","add-calculated-field","add-parameter","add-filter",
+ "add-dataParameter","add-order","add-selection","add-dataSetLink",
+ "add-dataSet","add-variant","add-conditionalAppearance",
+ "set-query","set-outputParameter","set-structure",
+ "modify-field","modify-filter","modify-dataParameter",
+ "clear-selection","clear-order","clear-filter",
+ "remove-field","remove-total","remove-calculated-field","remove-parameter","remove-filter")]
+ [string]$Operation,
+
+ [Parameter(Mandatory)]
+ [string]$Value,
+
+ [string]$DataSet,
+ [string]$Variant,
+ [switch]$NoSelection
+)
+
+$ErrorActionPreference = "Stop"
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+# --- 1. 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
+
+function Esc-Xml {
+ param([string]$s)
+ return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
+}
+
+# --- 2. Type system (copied from skd-compile) ---
+
+$script:typeSynonyms = New-Object System.Collections.Hashtable
+$script:typeSynonyms["число"] = "decimal"
+$script:typeSynonyms["строка"] = "string"
+$script:typeSynonyms["булево"] = "boolean"
+$script:typeSynonyms["дата"] = "date"
+$script:typeSynonyms["датавремя"] = "dateTime"
+$script:typeSynonyms["стандартныйпериод"] = "StandardPeriod"
+$script:typeSynonyms["bool"] = "boolean"
+$script:typeSynonyms["str"] = "string"
+$script:typeSynonyms["int"] = "decimal"
+$script:typeSynonyms["integer"] = "decimal"
+$script:typeSynonyms["number"] = "decimal"
+$script:typeSynonyms["num"] = "decimal"
+$script:typeSynonyms["справочникссылка"] = "CatalogRef"
+$script:typeSynonyms["документссылка"] = "DocumentRef"
+$script:typeSynonyms["перечислениессылка"] = "EnumRef"
+$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef"
+$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef"
+
+$script:outputParamTypes = @{
+ "Заголовок" = "mltext"
+ "ВыводитьЗаголовок" = "dcsset:DataCompositionTextOutputType"
+ "ВыводитьПараметрыДанных" = "dcsset:DataCompositionTextOutputType"
+ "ВыводитьОтбор" = "dcsset:DataCompositionTextOutputType"
+ "МакетОформления" = "xs:string"
+ "РасположениеПолейГруппировки" = "dcsset:DataCompositionGroupFieldsPlacement"
+ "РасположениеРеквизитов" = "dcsset:DataCompositionAttributesPlacement"
+ "ГоризонтальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement"
+ "ВертикальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement"
+}
+
+function Resolve-TypeStr {
+ param([string]$typeStr)
+ if (-not $typeStr) { return $typeStr }
+
+ if ($typeStr -match '^([^(]+)\((.+)\)$') {
+ $baseName = $Matches[1].Trim()
+ $params = $Matches[2]
+ $resolved = $script:typeSynonyms[$baseName.ToLower()]
+ if ($resolved) { return "$resolved($params)" }
+ return $typeStr
+ }
+
+ if ($typeStr.Contains('.')) {
+ $dotIdx = $typeStr.IndexOf('.')
+ $prefix = $typeStr.Substring(0, $dotIdx)
+ $suffix = $typeStr.Substring($dotIdx)
+ $resolved = $script:typeSynonyms[$prefix.ToLower()]
+ if ($resolved) { return "$resolved$suffix" }
+ return $typeStr
+ }
+
+ $resolved = $script:typeSynonyms[$typeStr.ToLower()]
+ if ($resolved) { return $resolved }
+ return $typeStr
+}
+
+# --- 3. Parsers ---
+
+function Parse-FieldShorthand {
+ param([string]$s)
+
+ $result = @{
+ dataPath = ""; field = ""; title = ""; type = ""
+ roles = @(); restrict = @()
+ }
+
+ # Extract [Title]
+ if ($s -match '\[([^\]]+)\]') {
+ $result.title = $Matches[1]
+ $s = $s -replace '\s*\[[^\]]+\]', ''
+ }
+
+ # Extract @roles
+ $roleMatches = [regex]::Matches($s, '@(\w+)')
+ foreach ($m in $roleMatches) {
+ $result.roles += $m.Groups[1].Value
+ }
+ $s = [regex]::Replace($s, '\s*@\w+', '')
+
+ # Extract #restrictions
+ $restrictMatches = [regex]::Matches($s, '#(\w+)')
+ foreach ($m in $restrictMatches) {
+ $result.restrict += $m.Groups[1].Value
+ }
+ $s = [regex]::Replace($s, '\s*#\w+', '')
+
+ # Split name: type
+ $s = $s.Trim()
+ if ($s.Contains(':')) {
+ $parts = $s -split ':', 2
+ $result.dataPath = $parts[0].Trim()
+ $result.type = Resolve-TypeStr ($parts[1].Trim())
+ } else {
+ $result.dataPath = $s
+ }
+
+ $result.field = $result.dataPath
+ return $result
+}
+
+function Read-FieldProperties($fieldEl) {
+ $props = @{
+ dataPath = ""; field = ""; title = ""; type = ""
+ roles = @(); restrict = @()
+ }
+
+ foreach ($ch in $fieldEl.ChildNodes) {
+ if ($ch.NodeType -ne 'Element') { continue }
+ switch ($ch.LocalName) {
+ "dataPath" { $props.dataPath = $ch.InnerText.Trim() }
+ "field" { $props.field = $ch.InnerText.Trim() }
+ "title" {
+ # Extract text from LocalStringType
+ foreach ($item in $ch.ChildNodes) {
+ if ($item.NodeType -eq 'Element' -and $item.LocalName -eq 'item') {
+ foreach ($gc in $item.ChildNodes) {
+ if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'content') {
+ $props.title = $gc.InnerText.Trim()
+ }
+ }
+ }
+ }
+ }
+ "valueType" {
+ # Read type info — store the raw element for now, we'll use type from parsed if overridden
+ $typeEl = $null
+ foreach ($gc in $ch.ChildNodes) {
+ if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'Type') {
+ $typeEl = $gc; break
+ }
+ }
+ if ($typeEl) {
+ $props["_rawTypeText"] = $typeEl.InnerText.Trim()
+ }
+ }
+ "role" {
+ foreach ($gc in $ch.ChildNodes) {
+ if ($gc.NodeType -eq 'Element') {
+ if ($gc.LocalName -eq 'periodNumber') {
+ $props.roles += "period"
+ } elseif ($gc.InnerText.Trim() -eq 'true') {
+ $props.roles += $gc.LocalName
+ }
+ }
+ }
+ }
+ "useRestriction" {
+ $revMap = @{ "field" = "noField"; "condition" = "noFilter"; "group" = "noGroup"; "order" = "noOrder" }
+ foreach ($gc in $ch.ChildNodes) {
+ if ($gc.NodeType -eq 'Element' -and $gc.InnerText.Trim() -eq 'true') {
+ $mapped = $revMap[$gc.LocalName]
+ if ($mapped) { $props.restrict += $mapped }
+ }
+ }
+ }
+ }
+ }
+ return $props
+}
+
+function Parse-TotalShorthand {
+ param([string]$s)
+
+ $parts = $s -split ':', 2
+ $dataPath = $parts[0].Trim()
+ $funcPart = $parts[1].Trim()
+
+ if ($funcPart -match '^\w+\(') {
+ return @{ dataPath = $dataPath; expression = $funcPart }
+ } else {
+ return @{ dataPath = $dataPath; expression = "$funcPart($dataPath)" }
+ }
+}
+
+function Parse-CalcShorthand {
+ param([string]$s)
+
+ $title = ""
+ # Extract [Title] first
+ if ($s -match '\[([^\]]+)\]') {
+ $title = $Matches[1]
+ $s = $s -replace '\s*\[[^\]]+\]', ''
+ }
+
+ # Support "Name: Type = Expression" and "Name = Expression"
+ $eqIdx = $s.IndexOf('=')
+ if ($eqIdx -gt 0) {
+ $left = $s.Substring(0, $eqIdx).Trim()
+ $expression = $s.Substring($eqIdx + 1).Trim()
+
+ if ($left.Contains(':')) {
+ $colonIdx = $left.IndexOf(':')
+ $dataPath = $left.Substring(0, $colonIdx).Trim()
+ $type = Resolve-TypeStr ($left.Substring($colonIdx + 1).Trim())
+ return @{ dataPath = $dataPath; expression = $expression; type = $type; title = $title }
+ }
+ return @{ dataPath = $left; expression = $expression; type = ""; title = $title }
+ }
+ return @{ dataPath = $s.Trim(); expression = ""; type = ""; title = $title }
+}
+
+function Parse-ParamShorthand {
+ param([string]$s)
+
+ $result = @{ name = ""; type = ""; value = $null; autoDates = $false }
+
+ if ($s -match '@autoDates') {
+ $result.autoDates = $true
+ $s = $s -replace '\s*@autoDates', ''
+ }
+
+ if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.+))?$') {
+ $result.name = $Matches[1].Trim()
+ $result.type = Resolve-TypeStr ($Matches[2].Trim())
+ if ($Matches[4]) {
+ $result.value = $Matches[4].Trim()
+ }
+ } else {
+ $result.name = $s.Trim()
+ }
+
+ return $result
+}
+
+function Parse-FilterShorthand {
+ param([string]$s)
+
+ $result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null }
+
+ if ($s -match '@user') {
+ $result.userSettingID = "auto"
+ $s = $s -replace '\s*@user', ''
+ }
+ if ($s -match '@off') {
+ $result.use = $false
+ $s = $s -replace '\s*@off', ''
+ }
+ if ($s -match '@quickAccess') {
+ $result.viewMode = "QuickAccess"
+ $s = $s -replace '\s*@quickAccess', ''
+ }
+ if ($s -match '@normal') {
+ $result.viewMode = "Normal"
+ $s = $s -replace '\s*@normal', ''
+ }
+ if ($s -match '@inaccessible') {
+ $result.viewMode = "Inaccessible"
+ $s = $s -replace '\s*@inaccessible', ''
+ }
+
+ $s = $s.Trim()
+
+ $opPatterns = @('<>', '>=', '<=', '=', '>', '<',
+ 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b',
+ 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b',
+ 'notFilled\b', 'filled\b')
+ $opJoined = $opPatterns -join '|'
+
+ if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") {
+ $result.field = $Matches[1].Trim()
+ $opRaw = $Matches[2].Trim()
+ $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" }
+
+ $opMap = @{
+ "=" = "Equal"; "<>" = "NotEqual"; ">" = "Greater"; ">=" = "GreaterOrEqual"
+ "<" = "Less"; "<=" = "LessOrEqual"; "in" = "InList"; "notIn" = "NotInList"
+ "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy"
+ "contains" = "Contains"; "notContains" = "NotContains"
+ "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith"
+ "filled" = "Filled"; "notFilled" = "NotFilled"
+ }
+ $mapped = $opMap[$opRaw]
+ if ($mapped) { $result.op = $mapped } else { $result.op = $opRaw }
+
+ if ($valPart -and $valPart -ne "_") {
+ if ($valPart -eq "true" -or $valPart -eq "false") {
+ $result.value = $valPart
+ $result["valueType"] = "xs:boolean"
+ } elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') {
+ $result.value = $valPart
+ $result["valueType"] = "xs:dateTime"
+ } elseif ($valPart -match '^\d+(\.\d+)?$') {
+ $result.value = $valPart
+ $result["valueType"] = "xs:decimal"
+ } else {
+ $result.value = $valPart
+ $result["valueType"] = "xs:string"
+ }
+ }
+ } else {
+ $result.field = $s
+ }
+
+ return $result
+}
+
+function Parse-DataParamShorthand {
+ param([string]$s)
+
+ $result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null }
+
+ if ($s -match '@user') {
+ $result.userSettingID = "auto"
+ $s = $s -replace '\s*@user', ''
+ }
+ if ($s -match '@off') {
+ $result.use = $false
+ $s = $s -replace '\s*@off', ''
+ }
+ if ($s -match '@quickAccess') {
+ $result.viewMode = "QuickAccess"
+ $s = $s -replace '\s*@quickAccess', ''
+ }
+ if ($s -match '@normal') {
+ $result.viewMode = "Normal"
+ $s = $s -replace '\s*@normal', ''
+ }
+
+ $s = $s.Trim()
+
+ if ($s -match '^([^=]+)=\s*(.+)$') {
+ $result.parameter = $Matches[1].Trim()
+ $valStr = $Matches[2].Trim()
+
+ $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear")
+ if ($periodVariants -contains $valStr) {
+ $result.value = @{ variant = $valStr }
+ } elseif ($valStr -match '^\d{4}-\d{2}-\d{2}T') {
+ $result.value = $valStr
+ } elseif ($valStr -eq "true" -or $valStr -eq "false") {
+ $result.value = $valStr
+ } else {
+ $result.value = $valStr
+ }
+ } else {
+ $result.parameter = $s
+ }
+
+ return $result
+}
+
+function Parse-OrderShorthand {
+ param([string]$s)
+ $s = $s.Trim()
+ if ($s -eq "Auto") {
+ return @{ field = "Auto"; direction = "" }
+ }
+ $parts = $s -split '\s+', 2
+ $field = $parts[0]
+ $dir = "Asc"
+ if ($parts.Count -gt 1 -and $parts[1] -match '(?i)^desc$') { $dir = "Desc" }
+ return @{ field = $field; direction = $dir }
+}
+
+function Parse-DataSetLinkShorthand {
+ param([string]$s)
+
+ $result = @{ source = ""; dest = ""; sourceExpr = ""; destExpr = ""; parameter = "" }
+
+ # Extract optional [param ParamName]
+ if ($s -match '\[param\s+([^\]]+)\]') {
+ $result.parameter = $Matches[1].Trim()
+ $s = $s -replace '\s*\[param\s+[^\]]+\]', ''
+ }
+
+ # Pattern: "Source > Dest on FieldA = FieldB"
+ if ($s -match '^(.+?)\s*>\s*(.+?)\s+on\s+(.+?)\s*=\s*(.+)$') {
+ $result.source = $Matches[1].Trim()
+ $result.dest = $Matches[2].Trim()
+ $result.sourceExpr = $Matches[3].Trim()
+ $result.destExpr = $Matches[4].Trim()
+ } else {
+ Write-Error "Invalid dataSetLink shorthand: $s. Expected: 'Source > Dest on FieldA = FieldB [param Name]'"
+ exit 1
+ }
+
+ return $result
+}
+
+function Parse-DataSetShorthand {
+ param([string]$s)
+
+ $s = $s.Trim()
+ # "Name: QUERY" — split on first ": " only if prefix is a single word (no spaces)
+ if ($s -match '^(\S+):\s(.+)$') {
+ return @{ name = $Matches[1]; query = $Matches[2] }
+ }
+ return @{ name = ""; query = $s }
+}
+
+function Parse-VariantShorthand {
+ param([string]$s)
+
+ $presentation = ""
+ if ($s -match '\[([^\]]+)\]') {
+ $presentation = $Matches[1]
+ $s = $s -replace '\s*\[[^\]]+\]', ''
+ }
+ $name = $s.Trim()
+ if (-not $presentation) { $presentation = $name }
+ return @{ name = $name; presentation = $presentation }
+}
+
+function Parse-ConditionalAppearanceShorthand {
+ param([string]$s)
+
+ $result = @{ param = ""; value = ""; filter = $null; fields = @() }
+
+ # Extract " when ..." — condition part
+ $whenIdx = $s.IndexOf(' when ')
+ $forIdx = $s.IndexOf(' for ')
+
+ # Determine boundaries
+ $mainEnd = $s.Length
+ if ($whenIdx -ge 0 -and $forIdx -ge 0) {
+ $mainEnd = [Math]::Min($whenIdx, $forIdx)
+ } elseif ($whenIdx -ge 0) {
+ $mainEnd = $whenIdx
+ } elseif ($forIdx -ge 0) {
+ $mainEnd = $forIdx
+ }
+
+ # Parse "for" fields
+ if ($forIdx -ge 0) {
+ $forEnd = $s.Length
+ if ($whenIdx -gt $forIdx) { $forEnd = $whenIdx }
+ $forPart = $s.Substring($forIdx + 5, $forEnd - $forIdx - 5).Trim()
+ $result.fields = @($forPart -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
+ }
+
+ # Parse "when" filter
+ if ($whenIdx -ge 0) {
+ $whenEnd = $s.Length
+ if ($forIdx -gt $whenIdx) { $whenEnd = $forIdx }
+ $whenPart = $s.Substring($whenIdx + 6, $whenEnd - $whenIdx - 6).Trim()
+ $result.filter = Parse-FilterShorthand $whenPart
+ }
+
+ # Parse main part: "Param = Value"
+ $mainPart = $s.Substring(0, $mainEnd).Trim()
+ $eqIdx = $mainPart.IndexOf('=')
+ if ($eqIdx -gt 0) {
+ $result.param = $mainPart.Substring(0, $eqIdx).Trim()
+ $result.value = $mainPart.Substring($eqIdx + 1).Trim()
+ } else {
+ $result.param = $mainPart
+ }
+
+ return $result
+}
+
+function Parse-StructureShorthand {
+ param([string]$s)
+
+ $segments = $s -split '\s*>\s*'
+ $result = @()
+
+ $innermost = $null
+ for ($i = $segments.Count - 1; $i -ge 0; $i--) {
+ $seg = $segments[$i].Trim()
+ $group = @{ type = "group" }
+
+ if ($seg -match '^(?i)(details|детали)$') {
+ $group["groupBy"] = @()
+ } else {
+ $group["groupBy"] = @($seg)
+ }
+
+ if ($null -ne $innermost) {
+ $group["children"] = @($innermost)
+ }
+ $innermost = $group
+ }
+
+ if ($innermost) { $result += $innermost }
+ return ,$result
+}
+
+function Parse-OutputParamShorthand {
+ param([string]$s)
+ $idx = $s.IndexOf('=')
+ if ($idx -gt 0) {
+ return @{
+ key = $s.Substring(0, $idx).Trim()
+ value = $s.Substring($idx + 1).Trim()
+ }
+ }
+ return @{ key = $s.Trim(); value = "" }
+}
+
+# --- 4. Build-* functions (XML fragment generators) ---
+
+function Build-ValueTypeXml {
+ param([string]$typeStr, [string]$indent)
+
+ if (-not $typeStr) { return "" }
+ $typeStr = Resolve-TypeStr $typeStr
+ $lines = @()
+
+ if ($typeStr -eq "boolean") {
+ $lines += "$indentxs:boolean"
+ return $lines -join "`r`n"
+ }
+
+ if ($typeStr -match '^string(\((\d+)\))?$') {
+ $len = if ($Matches[2]) { $Matches[2] } else { "0" }
+ $lines += "$indentxs:string"
+ $lines += "$indent"
+ $lines += "$indent`t$len"
+ $lines += "$indent`tVariable"
+ $lines += "$indent"
+ return $lines -join "`r`n"
+ }
+
+ if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') {
+ $digits = $Matches[1]
+ $fraction = $Matches[2]
+ $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" }
+ $lines += "$indentxs:decimal"
+ $lines += "$indent"
+ $lines += "$indent`t$digits"
+ $lines += "$indent`t$fraction"
+ $lines += "$indent`t$sign"
+ $lines += "$indent"
+ return $lines -join "`r`n"
+ }
+
+ if ($typeStr -match '^(date|dateTime)$') {
+ $fractions = switch ($typeStr) {
+ "date" { "Date" }
+ "dateTime" { "DateTime" }
+ }
+ $lines += "$indentxs:dateTime"
+ $lines += "$indent"
+ $lines += "$indent`t$fractions"
+ $lines += "$indent"
+ return $lines -join "`r`n"
+ }
+
+ if ($typeStr -eq "StandardPeriod") {
+ $lines += "$indentv8:StandardPeriod"
+ return $lines -join "`r`n"
+ }
+
+ if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') {
+ $lines += "$indentd5p1:$(Esc-Xml $typeStr)"
+ return $lines -join "`r`n"
+ }
+
+ if ($typeStr.Contains('.')) {
+ $lines += "$indentd5p1:$(Esc-Xml $typeStr)"
+ return $lines -join "`r`n"
+ }
+
+ $lines += "$indent$(Esc-Xml $typeStr)"
+ return $lines -join "`r`n"
+}
+
+function Build-MLTextXml {
+ param([string]$tag, [string]$text, [string]$indent)
+ $lines = @()
+ $lines += "$indent<$tag xsi:type=`"v8:LocalStringType`">"
+ $lines += "$indent`t"
+ $lines += "$indent`t`tru"
+ $lines += "$indent`t`t$(Esc-Xml $text)"
+ $lines += "$indent`t"
+ $lines += "$indent$tag>"
+ return $lines -join "`r`n"
+}
+
+function Build-RoleXml {
+ param([string[]]$roles, [string]$indent)
+
+ if (-not $roles -or $roles.Count -eq 0) { return "" }
+
+ $lines = @()
+ $lines += "$indent"
+ foreach ($role in $roles) {
+ if ($role -eq "period") {
+ $lines += "$indent`t1"
+ $lines += "$indent`tMain"
+ } else {
+ $lines += "$indent`ttrue"
+ }
+ }
+ $lines += "$indent"
+ return $lines -join "`r`n"
+}
+
+function Build-RestrictionXml {
+ param([string[]]$restrict, [string]$indent)
+
+ if (-not $restrict -or $restrict.Count -eq 0) { return "" }
+
+ $restrictMap = @{
+ "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition"
+ "noGroup" = "group"; "noOrder" = "order"
+ }
+
+ $lines = @()
+ $lines += "$indent"
+ foreach ($r in $restrict) {
+ $xmlName = $restrictMap["$r"]
+ if ($xmlName) {
+ $lines += "$indent`t<$xmlName>true$xmlName>"
+ }
+ }
+ $lines += "$indent"
+ return $lines -join "`r`n"
+}
+
+function Build-FieldFragment {
+ param($parsed, [string]$indent)
+
+ $i = $indent
+ $lines = @()
+ $lines += "$i"
+ $lines += "$i`t$(Esc-Xml $parsed.dataPath)"
+ $lines += "$i`t$(Esc-Xml $parsed.field)"
+
+ if ($parsed.title) {
+ $lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t")
+ }
+
+ if ($parsed.restrict -and $parsed.restrict.Count -gt 0) {
+ $lines += (Build-RestrictionXml -restrict $parsed.restrict -indent "$i`t")
+ }
+
+ $roleXml = Build-RoleXml -roles $parsed.roles -indent "$i`t"
+ if ($roleXml) { $lines += $roleXml }
+
+ if ($parsed.type) {
+ $lines += "$i`t"
+ $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t")
+ $lines += "$i`t"
+ }
+
+ $lines += "$i"
+ return $lines -join "`r`n"
+}
+
+function Build-TotalFragment {
+ param($parsed, [string]$indent)
+
+ $i = $indent
+ $lines = @()
+ $lines += "$i"
+ $lines += "$i`t$(Esc-Xml $parsed.dataPath)"
+ $lines += "$i`t$(Esc-Xml $parsed.expression)"
+ $lines += "$i"
+ return $lines -join "`r`n"
+}
+
+function Build-CalcFieldFragment {
+ param($parsed, [string]$indent)
+
+ $i = $indent
+ $lines = @()
+ $lines += "$i"
+ $lines += "$i`t$(Esc-Xml $parsed.dataPath)"
+ $lines += "$i`t$(Esc-Xml $parsed.expression)"
+
+ if ($parsed.title) {
+ $lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t")
+ }
+
+ if ($parsed.type) {
+ $lines += "$i`t"
+ $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t")
+ $lines += "$i`t"
+ }
+
+ $lines += "$i"
+ return $lines -join "`r`n"
+}
+
+function Build-ParamFragment {
+ param($parsed, [string]$indent)
+
+ $i = $indent
+ $fragments = @()
+
+ $lines = @()
+ $lines += "$i"
+ $lines += "$i`t$(Esc-Xml $parsed.name)"
+
+ if ($parsed.type) {
+ $lines += "$i`t"
+ $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t")
+ $lines += "$i`t"
+ }
+
+ if ($null -ne $parsed.value) {
+ $valStr = "$($parsed.value)"
+ if ($parsed.type -eq "StandardPeriod") {
+ $lines += "$i`t"
+ $lines += "$i`t`t$(Esc-Xml $valStr)"
+ $lines += "$i`t"
+ } elseif ($parsed.type -match '^date') {
+ $lines += "$i`t$(Esc-Xml $valStr)"
+ } elseif ($parsed.type -eq "boolean") {
+ $lines += "$i`t$(Esc-Xml $valStr)"
+ } elseif ($parsed.type -match '^decimal') {
+ $lines += "$i`t$(Esc-Xml $valStr)"
+ } else {
+ $lines += "$i`t$(Esc-Xml $valStr)"
+ }
+ }
+
+ $lines += "$i"
+ $fragments += ($lines -join "`r`n")
+
+ if ($parsed.autoDates) {
+ $paramName = $parsed.name
+
+ $bLines = @()
+ $bLines += "$i"
+ $bLines += "$i`tДатаНачала"
+ $bLines += "$i`t"
+ $bLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t")
+ $bLines += "$i`t"
+ $bLines += "$i`t$(Esc-Xml "&$paramName.ДатаНачала")"
+ $bLines += "$i`tfalse"
+ $bLines += "$i"
+ $fragments += ($bLines -join "`r`n")
+
+ $eLines = @()
+ $eLines += "$i"
+ $eLines += "$i`tДатаОкончания"
+ $eLines += "$i`t"
+ $eLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t")
+ $eLines += "$i`t"
+ $eLines += "$i`t$(Esc-Xml "&$paramName.ДатаОкончания")"
+ $eLines += "$i`tfalse"
+ $eLines += "$i"
+ $fragments += ($eLines -join "`r`n")
+ }
+
+ return ,$fragments
+}
+
+function Build-FilterItemFragment {
+ param($parsed, [string]$indent)
+
+ $i = $indent
+ $lines = @()
+ $lines += "$i"
+
+ if ($parsed.use -eq $false) {
+ $lines += "$i`tfalse"
+ }
+
+ $lines += "$i`t$(Esc-Xml $parsed.field)"
+ $lines += "$i`t$(Esc-Xml $parsed.op)"
+
+ if ($null -ne $parsed.value) {
+ $vt = if ($parsed["valueType"]) { $parsed["valueType"] } else { "xs:string" }
+ $lines += "$i`t$(Esc-Xml "$($parsed.value)")"
+ }
+
+ if ($parsed.viewMode) {
+ $lines += "$i`t$(Esc-Xml $parsed.viewMode)"
+ }
+
+ if ($parsed.userSettingID) {
+ $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID }
+ $lines += "$i`t$(Esc-Xml $uid)"
+ }
+
+ $lines += "$i"
+ return $lines -join "`r`n"
+}
+
+function Build-SelectionItemFragment {
+ param([string]$fieldName, [string]$indent)
+
+ $i = $indent
+ $lines = @()
+ if ($fieldName -eq "Auto") {
+ $lines += "$i"
+ } else {
+ $lines += "$i"
+ $lines += "$i`t$(Esc-Xml $fieldName)"
+ $lines += "$i"
+ }
+ return $lines -join "`r`n"
+}
+
+function Build-DataParamFragment {
+ param($parsed, [string]$indent)
+
+ $i = $indent
+ $lines = @()
+ $lines += "$i"
+
+ if ($parsed.use -eq $false) {
+ $lines += "$i`tfalse"
+ }
+
+ $lines += "$i`t$(Esc-Xml $parsed.parameter)"
+
+ if ($null -ne $parsed.value) {
+ if ($parsed.value -is [hashtable] -and $parsed.value.variant) {
+ $lines += "$i`t"
+ $lines += "$i`t`t$(Esc-Xml $parsed.value.variant)"
+ $lines += "$i`t"
+ } elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') {
+ $lines += "$i`t$(Esc-Xml "$($parsed.value)")"
+ } elseif ("$($parsed.value)" -eq "true" -or "$($parsed.value)" -eq "false") {
+ $lines += "$i`t$(Esc-Xml "$($parsed.value)")"
+ } else {
+ $lines += "$i`t$(Esc-Xml "$($parsed.value)")"
+ }
+ }
+
+ if ($parsed.viewMode) {
+ $lines += "$i`t$(Esc-Xml $parsed.viewMode)"
+ }
+
+ if ($parsed.userSettingID) {
+ $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID }
+ $lines += "$i`t$(Esc-Xml $uid)"
+ }
+
+ $lines += "$i"
+ return $lines -join "`r`n"
+}
+
+function Build-OrderItemFragment {
+ param($parsed, [string]$indent)
+
+ $i = $indent
+ $lines = @()
+ if ($parsed.field -eq "Auto") {
+ $lines += "$i"
+ } else {
+ $lines += "$i"
+ $lines += "$i`t$(Esc-Xml $parsed.field)"
+ $lines += "$i`t$($parsed.direction)"
+ $lines += "$i"
+ }
+ return $lines -join "`r`n"
+}
+
+function Build-DataSetLinkFragment {
+ param($parsed, [string]$indent)
+
+ $i = $indent
+ $lines = @()
+ $lines += "$i"
+ $lines += "$i`t$(Esc-Xml $parsed.source)"
+ $lines += "$i`t$(Esc-Xml $parsed.dest)"
+ $lines += "$i`t$(Esc-Xml $parsed.sourceExpr)"
+ $lines += "$i`t$(Esc-Xml $parsed.destExpr)"
+ if ($parsed.parameter) {
+ $lines += "$i`t$(Esc-Xml $parsed.parameter)"
+ }
+ $lines += "$i"
+ return $lines -join "`r`n"
+}
+
+function Build-DataSetQueryFragment {
+ param($parsed, [string]$indent)
+
+ $i = $indent
+ $lines = @()
+ $lines += "$i"
+ $lines += "$i`t$(Esc-Xml $parsed.name)"
+ $lines += "$i`t$(Esc-Xml $parsed.dataSource)"
+ $lines += "$i`t$(Esc-Xml $parsed.query)"
+ $lines += "$i"
+ return $lines -join "`r`n"
+}
+
+function Build-VariantFragment {
+ param($parsed, [string]$indent)
+
+ $i = $indent
+ $lines = @()
+ $lines += "$i"
+ $lines += "$i`t$(Esc-Xml $parsed.name)"
+ $lines += (Build-MLTextXml -tag "dcsset:presentation" -text $parsed.presentation -indent "$i`t")
+ $lines += "$i`t"
+ $lines += "$i`t`t"
+ $lines += "$i`t`t`t"
+ $lines += "$i`t`t"
+ $lines += "$i`t`t"
+ $lines += "$i`t`t`t"
+ $lines += "$i`t`t`t"
+ $lines += "$i`t`t`t`t"
+ $lines += "$i`t`t`t"
+ $lines += "$i`t`t`t"
+ $lines += "$i`t`t`t`t"
+ $lines += "$i`t`t`t"
+ $lines += "$i`t`t"
+ $lines += "$i`t"
+ $lines += "$i"
+ return $lines -join "`r`n"
+}
+
+function Build-ConditionalAppearanceItemFragment {
+ param($parsed, [string]$indent)
+
+ $i = $indent
+ $lines = @()
+ $lines += "$i"
+
+ # selection
+ if ($parsed.fields -and $parsed.fields.Count -gt 0) {
+ $lines += "$i`t"
+ foreach ($fld in $parsed.fields) {
+ $lines += "$i`t`t"
+ $lines += "$i`t`t`t$(Esc-Xml $fld)"
+ $lines += "$i`t`t"
+ }
+ $lines += "$i`t"
+ } else {
+ $lines += "$i`t"
+ }
+
+ # filter
+ if ($parsed.filter) {
+ $lines += "$i`t"
+ $f = $parsed.filter
+ $lines += "$i`t`t"
+ $lines += "$i`t`t`t$(Esc-Xml $f.field)"
+ $lines += "$i`t`t`t$(Esc-Xml $f.op)"
+ if ($null -ne $f.value) {
+ $vt = if ($f["valueType"]) { $f["valueType"] } else { "xs:string" }
+ $lines += "$i`t`t`t$(Esc-Xml "$($f.value)")"
+ }
+ $lines += "$i`t`t"
+ $lines += "$i`t"
+ } else {
+ $lines += "$i`t"
+ }
+
+ # appearance
+ $lines += "$i`t"
+
+ # Auto-detect value type
+ $val = $parsed.value
+ $valType = "xs:string"
+ if ($val -match '^(web|style|win):') {
+ $valType = "v8ui:Color"
+ } elseif ($val -eq "true" -or $val -eq "false") {
+ $valType = "xs:boolean"
+ }
+
+ $lines += "$i`t`t"
+ $lines += "$i`t`t`t$(Esc-Xml $parsed.param)"
+ $lines += "$i`t`t`t$(Esc-Xml $val)"
+ $lines += "$i`t`t"
+ $lines += "$i`t"
+
+ $lines += "$i"
+ return $lines -join "`r`n"
+}
+
+function Build-StructureItemFragment {
+ param($item, [string]$indent)
+
+ $i = $indent
+ $lines = @()
+ $lines += "$i"
+
+ # groupItems
+ $groupBy = $item["groupBy"]
+ if (-not $groupBy -or $groupBy.Count -eq 0) {
+ $lines += "$i`t"
+ } else {
+ $lines += "$i`t"
+ foreach ($field in $groupBy) {
+ $lines += "$i`t`t"
+ $lines += "$i`t`t`t$(Esc-Xml $field)"
+ $lines += "$i`t`t`tItems"
+ $lines += "$i`t`t`tNone"
+ $lines += "$i`t`t`t0001-01-01T00:00:00"
+ $lines += "$i`t`t`t0001-01-01T00:00:00"
+ $lines += "$i`t`t"
+ }
+ $lines += "$i`t"
+ }
+
+ # order (Auto)
+ $lines += "$i`t"
+ $lines += "$i`t`t"
+ $lines += "$i`t"
+
+ # selection (Auto)
+ $lines += "$i`t"
+ $lines += "$i`t`t"
+ $lines += "$i`t"
+
+ # Recursive children
+ if ($item["children"]) {
+ foreach ($child in $item["children"]) {
+ $childXml = Build-StructureItemFragment -item $child -indent "$i`t"
+ $lines += $childXml
+ }
+ }
+
+ $lines += "$i"
+ return $lines -join "`r`n"
+}
+
+function Build-OutputParamFragment {
+ param($parsed, [string]$indent)
+
+ $i = $indent
+ $key = $parsed.key
+ $val = $parsed.value
+ $ptype = $script:outputParamTypes[$key]
+ if (-not $ptype) { $ptype = "xs:string" }
+
+ $lines = @()
+ $lines += "$i"
+ $lines += "$i`t$(Esc-Xml $key)"
+
+ if ($ptype -eq "mltext") {
+ $lines += "$i`t"
+ $lines += "$i`t`t"
+ $lines += "$i`t`t`tru"
+ $lines += "$i`t`t`t$(Esc-Xml $val)"
+ $lines += "$i`t`t"
+ $lines += "$i`t"
+ } else {
+ $lines += "$i`t$(Esc-Xml $val)"
+ }
+
+ $lines += "$i"
+ return $lines -join "`r`n"
+}
+
+# --- 5. XML helpers ---
+
+function Import-Fragment($doc, [string]$xmlString) {
+ $wrapper = @"
+<_W xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:v8="http://v8.1c.ru/8.1/data/core"
+ xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
+ xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
+ xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
+ xmlns:v8ui="http://v8.1c.ru/8.1/data/ui">$xmlString
+"@
+ $frag = New-Object System.Xml.XmlDocument
+ $frag.PreserveWhitespace = $true
+ $frag.LoadXml($wrapper)
+ $nodes = @()
+ foreach ($child in $frag.DocumentElement.ChildNodes) {
+ if ($child.NodeType -eq 'Element') {
+ $nodes += $doc.ImportNode($child, $true)
+ }
+ }
+ return ,$nodes
+}
+
+function Get-ChildIndent($container) {
+ foreach ($child in $container.ChildNodes) {
+ if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') {
+ $text = $child.Value
+ if ($text -match '^\r?\n(\t+)$') { return $Matches[1] }
+ if ($text -match '^\r?\n(\t+)') { return $Matches[1] }
+ }
+ }
+ $depth = 0
+ $current = $container
+ while ($current -and $current -ne $xmlDoc.DocumentElement) {
+ $depth++
+ $current = $current.ParentNode
+ }
+ return "`t" * ($depth + 1)
+}
+
+function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) {
+ $ws = $xmlDoc.CreateWhitespace("`r`n$childIndent")
+ if ($refNode) {
+ $container.InsertBefore($ws, $refNode) | Out-Null
+ $container.InsertBefore($newNode, $ws) | Out-Null
+ } else {
+ $trailing = $container.LastChild
+ if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) {
+ $container.InsertBefore($ws, $trailing) | Out-Null
+ $container.InsertBefore($newNode, $trailing) | Out-Null
+ } else {
+ $container.AppendChild($ws) | Out-Null
+ $container.AppendChild($newNode) | Out-Null
+ $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" }
+ $closeWs = $xmlDoc.CreateWhitespace("`r`n$parentIndent")
+ $container.AppendChild($closeWs) | Out-Null
+ }
+ }
+}
+
+function Clear-ContainerChildren($container) {
+ $toRemove = @()
+ foreach ($child in $container.ChildNodes) {
+ if ($child.NodeType -eq 'Element') {
+ $toRemove += $child
+ }
+ }
+ foreach ($el in $toRemove) {
+ Remove-NodeWithWhitespace $el
+ }
+}
+
+function Remove-NodeWithWhitespace($node) {
+ $parent = $node.ParentNode
+ $prev = $node.PreviousSibling
+ $next = $node.NextSibling
+
+ if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) {
+ $parent.RemoveChild($prev) | Out-Null
+ } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) {
+ $parent.RemoveChild($next) | Out-Null
+ }
+ $parent.RemoveChild($node) | Out-Null
+}
+
+function Find-FirstElement($container, [string[]]$localNames, [string]$nsUri) {
+ foreach ($child in $container.ChildNodes) {
+ if ($child.NodeType -eq 'Element') {
+ foreach ($name in $localNames) {
+ if ($child.LocalName -eq $name) {
+ if (-not $nsUri -or $child.NamespaceURI -eq $nsUri) {
+ return $child
+ }
+ }
+ }
+ }
+ }
+ return $null
+}
+
+function Find-LastElement($container, [string]$localName, [string]$nsUri) {
+ $last = $null
+ foreach ($child in $container.ChildNodes) {
+ if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $localName) {
+ if (-not $nsUri -or $child.NamespaceURI -eq $nsUri) {
+ $last = $child
+ }
+ }
+ }
+ return $last
+}
+
+function Find-ElementByChildValue($container, [string]$elemName, [string]$childName, [string]$childValue, [string]$nsUri) {
+ foreach ($child in $container.ChildNodes) {
+ if ($child.NodeType -ne 'Element') { continue }
+ if ($child.LocalName -ne $elemName) { continue }
+ if ($nsUri -and $child.NamespaceURI -ne $nsUri) { continue }
+
+ foreach ($gc in $child.ChildNodes) {
+ if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq $childName -and $gc.InnerText.Trim() -eq $childValue) {
+ return $child
+ }
+ }
+ }
+ return $null
+}
+
+function Set-OrCreateChildElement($parent, [string]$localName, [string]$nsUri, [string]$value, [string]$indent) {
+ $existing = $null
+ foreach ($ch in $parent.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $localName -and $ch.NamespaceURI -eq $nsUri) {
+ $existing = $ch
+ break
+ }
+ }
+ if ($existing) {
+ $existing.InnerText = $value
+ } else {
+ $prefix = $parent.GetPrefixOfNamespace($nsUri)
+ $qualName = if ($prefix) { "${prefix}:$localName" } else { $localName }
+ $fragXml = "$indent<$qualName>$(Esc-Xml $value)$qualName>"
+ $nodes = Import-Fragment $xmlDoc $fragXml
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $parent $node $null $indent
+ }
+ }
+}
+
+function Set-OrCreateChildElementWithAttr($parent, [string]$localName, [string]$nsUri, [string]$value, [string]$xsiType, [string]$indent) {
+ $existing = $null
+ foreach ($ch in $parent.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $localName -and $ch.NamespaceURI -eq $nsUri) {
+ $existing = $ch
+ break
+ }
+ }
+ if ($existing) {
+ $existing.InnerText = $value
+ if ($xsiType) {
+ $existing.SetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance", $xsiType) | Out-Null
+ }
+ } else {
+ $prefix = $parent.GetPrefixOfNamespace($nsUri)
+ $qualName = if ($prefix) { "${prefix}:$localName" } else { $localName }
+ $typeAttr = if ($xsiType) { " xsi:type=`"$xsiType`"" } else { "" }
+ $fragXml = "$indent<$qualName$typeAttr>$(Esc-Xml $value)$qualName>"
+ $nodes = Import-Fragment $xmlDoc $fragXml
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $parent $node $null $indent
+ }
+ }
+}
+
+function Resolve-DataSet {
+ $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema"
+ $root = $xmlDoc.DocumentElement
+
+ if ($DataSet) {
+ foreach ($child in $root.ChildNodes) {
+ if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) {
+ $nameEl = $null
+ foreach ($gc in $child.ChildNodes) {
+ if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) {
+ $nameEl = $gc
+ break
+ }
+ }
+ if ($nameEl -and $nameEl.InnerText -eq $DataSet) {
+ return $child
+ }
+ }
+ }
+ Write-Error "DataSet '$DataSet' not found"
+ exit 1
+ }
+
+ foreach ($child in $root.ChildNodes) {
+ if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) {
+ return $child
+ }
+ }
+ Write-Error "No dataSet found in DCS"
+ exit 1
+}
+
+function Resolve-VariantSettings {
+ $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema"
+ $setNs = "http://v8.1c.ru/8.1/data-composition-system/settings"
+ $root = $xmlDoc.DocumentElement
+
+ $sv = $null
+ if ($Variant) {
+ foreach ($child in $root.ChildNodes) {
+ if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) {
+ $nameEl = $null
+ foreach ($gc in $child.ChildNodes) {
+ if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs) {
+ $nameEl = $gc
+ break
+ }
+ }
+ if ($nameEl -and $nameEl.InnerText -eq $Variant) {
+ $sv = $child
+ break
+ }
+ }
+ }
+ if (-not $sv) {
+ Write-Error "Variant '$Variant' not found"
+ exit 1
+ }
+ } else {
+ foreach ($child in $root.ChildNodes) {
+ if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) {
+ $sv = $child
+ break
+ }
+ }
+ if (-not $sv) {
+ Write-Error "No settingsVariant found in DCS"
+ exit 1
+ }
+ }
+
+ foreach ($gc in $sv.ChildNodes) {
+ if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'settings' -and $gc.NamespaceURI -eq $setNs) {
+ return $gc
+ }
+ }
+
+ Write-Error "No found in variant"
+ exit 1
+}
+
+function Ensure-SettingsChild($settings, [string]$childName, [string[]]$afterSiblings) {
+ $el = Find-FirstElement $settings @($childName) $setNs
+ if ($el) { return $el }
+
+ $indent = Get-ChildIndent $settings
+ $fragXml = "$indent"
+ $nodes = Import-Fragment $xmlDoc $fragXml
+
+ $refNode = $null
+ foreach ($sibName in $afterSiblings) {
+ $sib = Find-FirstElement $settings @($sibName) $setNs
+ if ($sib) {
+ $refNode = $sib.NextSibling
+ while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) {
+ $refNode = $refNode.NextSibling
+ }
+ break
+ }
+ }
+
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $settings $node $refNode $indent
+ }
+
+ return Find-FirstElement $settings @($childName) $setNs
+}
+
+function Get-VariantName {
+ $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema"
+ $setNs = "http://v8.1c.ru/8.1/data-composition-system/settings"
+ $root = $xmlDoc.DocumentElement
+
+ if ($Variant) { return $Variant }
+
+ foreach ($child in $root.ChildNodes) {
+ if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) {
+ foreach ($gc in $child.ChildNodes) {
+ if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs) {
+ return $gc.InnerText
+ }
+ }
+ }
+ }
+ return "(unknown)"
+}
+
+function Get-DataSetName($dsNode) {
+ $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema"
+ foreach ($gc in $dsNode.ChildNodes) {
+ if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) {
+ return $gc.InnerText
+ }
+ }
+ return "(unknown)"
+}
+
+function Get-ContainerChildIndent($container) {
+ $hasElements = $false
+ foreach ($ch in $container.ChildNodes) {
+ if ($ch.NodeType -eq 'Element') { $hasElements = $true; break }
+ }
+ if ($hasElements) {
+ return Get-ChildIndent $container
+ } else {
+ $parentIndent = Get-ChildIndent $container.ParentNode
+ return $parentIndent + "`t"
+ }
+}
+
+# --- 6. Load XML ---
+
+$xmlDoc = New-Object System.Xml.XmlDocument
+$xmlDoc.PreserveWhitespace = $true
+$xmlDoc.Load($resolvedPath)
+
+$schNs = "http://v8.1c.ru/8.1/data-composition-system/schema"
+$setNs = "http://v8.1c.ru/8.1/data-composition-system/settings"
+$corNs = "http://v8.1c.ru/8.1/data-composition-system/core"
+
+# --- 7. Batch value splitting ---
+
+if ($Operation -eq "set-query" -or $Operation -eq "set-structure" -or $Operation -eq "add-dataSet") {
+ $values = @($Value)
+} else {
+ $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
+}
+
+# --- 8. Main logic ---
+
+switch ($Operation) {
+ "add-field" {
+ $dsNode = Resolve-DataSet
+ $dsName = Get-DataSetName $dsNode
+
+ foreach ($val in $values) {
+ $parsed = Parse-FieldShorthand $val
+ $childIndent = Get-ChildIndent $dsNode
+
+ # Duplicate check
+ $existing = Find-ElementByChildValue $dsNode "field" "dataPath" $parsed.dataPath $schNs
+ if ($existing) {
+ Write-Host "[WARN] Field `"$($parsed.dataPath)`" already exists in dataset `"$dsName`" — skipped"
+ continue
+ }
+
+ $fragXml = Build-FieldFragment -parsed $parsed -indent $childIndent
+ $nodes = Import-Fragment $xmlDoc $fragXml
+
+ $refNode = Find-FirstElement $dsNode @("dataSource") $schNs
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $dsNode $node $refNode $childIndent
+ }
+
+ Write-Host "[OK] Field `"$($parsed.dataPath)`" added to dataset `"$dsName`""
+
+ if (-not $NoSelection) {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+ $selection = Ensure-SettingsChild $settings "selection" @()
+ $existingSel = Find-ElementByChildValue $selection "item" "field" $parsed.dataPath $setNs
+ if ($existingSel) {
+ Write-Host "[INFO] Field `"$($parsed.dataPath)`" already in selection — skipped"
+ } else {
+ $selIndent = Get-ContainerChildIndent $selection
+ $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent
+ $selNodes = Import-Fragment $xmlDoc $selXml
+ foreach ($node in $selNodes) {
+ Insert-BeforeElement $selection $node $null $selIndent
+ }
+ Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`""
+ }
+ }
+ }
+ }
+
+ "add-total" {
+ foreach ($val in $values) {
+ $parsed = Parse-TotalShorthand $val
+ $childIndent = Get-ChildIndent $xmlDoc.DocumentElement
+
+ # Duplicate check
+ $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "totalField" "dataPath" $parsed.dataPath $schNs
+ if ($existing) {
+ Write-Host "[WARN] TotalField `"$($parsed.dataPath)`" already exists — skipped"
+ continue
+ }
+
+ $fragXml = Build-TotalFragment -parsed $parsed -indent $childIndent
+ $nodes = Import-Fragment $xmlDoc $fragXml
+
+ $root = $xmlDoc.DocumentElement
+ $lastTotal = Find-LastElement $root "totalField" $schNs
+ if ($lastTotal) {
+ $refNode = $lastTotal.NextSibling
+ while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) {
+ $refNode = $refNode.NextSibling
+ }
+ } else {
+ $refNode = Find-FirstElement $root @("parameter","template","groupTemplate","settingsVariant") $schNs
+ }
+
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $root $node $refNode $childIndent
+ }
+
+ Write-Host "[OK] TotalField `"$($parsed.dataPath)`" = $($parsed.expression) added"
+ }
+ }
+
+ "add-calculated-field" {
+ foreach ($val in $values) {
+ $parsed = Parse-CalcShorthand $val
+ $childIndent = Get-ChildIndent $xmlDoc.DocumentElement
+
+ # Duplicate check
+ $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "calculatedField" "dataPath" $parsed.dataPath $schNs
+ if ($existing) {
+ Write-Host "[WARN] CalculatedField `"$($parsed.dataPath)`" already exists — skipped"
+ continue
+ }
+
+ $fragXml = Build-CalcFieldFragment -parsed $parsed -indent $childIndent
+ $nodes = Import-Fragment $xmlDoc $fragXml
+
+ $root = $xmlDoc.DocumentElement
+ $lastCalc = Find-LastElement $root "calculatedField" $schNs
+ if ($lastCalc) {
+ $refNode = $lastCalc.NextSibling
+ while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) {
+ $refNode = $refNode.NextSibling
+ }
+ } else {
+ $refNode = Find-FirstElement $root @("totalField","parameter","template","groupTemplate","settingsVariant") $schNs
+ }
+
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $root $node $refNode $childIndent
+ }
+
+ Write-Host "[OK] CalculatedField `"$($parsed.dataPath)`" = $($parsed.expression) added"
+
+ if (-not $NoSelection) {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+ $selection = Ensure-SettingsChild $settings "selection" @()
+ $existingSel = Find-ElementByChildValue $selection "item" "field" $parsed.dataPath $setNs
+ if ($existingSel) {
+ Write-Host "[INFO] Field `"$($parsed.dataPath)`" already in selection — skipped"
+ } else {
+ $selIndent = Get-ContainerChildIndent $selection
+ $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent
+ $selNodes = Import-Fragment $xmlDoc $selXml
+ foreach ($node in $selNodes) {
+ Insert-BeforeElement $selection $node $null $selIndent
+ }
+ Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`""
+ }
+ }
+ }
+ }
+
+ "add-parameter" {
+ foreach ($val in $values) {
+ $parsed = Parse-ParamShorthand $val
+ $childIndent = Get-ChildIndent $xmlDoc.DocumentElement
+
+ # Duplicate check
+ $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "parameter" "name" $parsed.name $schNs
+ if ($existing) {
+ Write-Host "[WARN] Parameter `"$($parsed.name)`" already exists — skipped"
+ continue
+ }
+
+ $fragments = Build-ParamFragment -parsed $parsed -indent $childIndent
+
+ $root = $xmlDoc.DocumentElement
+ $lastParam = Find-LastElement $root "parameter" $schNs
+ if ($lastParam) {
+ $refNode = $lastParam.NextSibling
+ while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) {
+ $refNode = $refNode.NextSibling
+ }
+ } else {
+ $refNode = Find-FirstElement $root @("template","groupTemplate","settingsVariant") $schNs
+ }
+
+ foreach ($fragXml in $fragments) {
+ $nodes = Import-Fragment $xmlDoc $fragXml
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $root $node $refNode $childIndent
+ }
+ }
+
+ Write-Host "[OK] Parameter `"$($parsed.name)`" added"
+ if ($parsed.autoDates) {
+ Write-Host "[OK] Auto-parameters `"ДатаНачала`", `"ДатаОкончания`" added"
+ }
+ }
+ }
+
+ "add-filter" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+
+ foreach ($val in $values) {
+ $parsed = Parse-FilterShorthand $val
+
+ $filterEl = Ensure-SettingsChild $settings "filter" @("selection")
+ $filterIndent = Get-ContainerChildIndent $filterEl
+
+ $fragXml = Build-FilterItemFragment -parsed $parsed -indent $filterIndent
+ $nodes = Import-Fragment $xmlDoc $fragXml
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $filterEl $node $null $filterIndent
+ }
+
+ Write-Host "[OK] Filter `"$($parsed.field) $($parsed.op)`" added to variant `"$varName`""
+ }
+ }
+
+ "add-dataParameter" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+
+ foreach ($val in $values) {
+ $parsed = Parse-DataParamShorthand $val
+
+ $dpEl = Ensure-SettingsChild $settings "dataParameters" @("outputParameters","conditionalAppearance","order","filter","selection")
+ $dpIndent = Get-ContainerChildIndent $dpEl
+
+ $fragXml = Build-DataParamFragment -parsed $parsed -indent $dpIndent
+ $nodes = Import-Fragment $xmlDoc $fragXml
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $dpEl $node $null $dpIndent
+ }
+
+ Write-Host "[OK] DataParameter `"$($parsed.parameter)`" added to variant `"$varName`""
+ }
+ }
+
+ "add-order" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+
+ foreach ($val in $values) {
+ $parsed = Parse-OrderShorthand $val
+
+ $orderEl = Ensure-SettingsChild $settings "order" @("filter","selection")
+ $orderIndent = Get-ContainerChildIndent $orderEl
+
+ # Duplicate check
+ if ($parsed.field -eq "Auto") {
+ $isDup = $false
+ foreach ($ch in $orderEl.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item') {
+ $typeAttr = $ch.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
+ if ($typeAttr -and $typeAttr.Contains("OrderItemAuto")) { $isDup = $true; break }
+ }
+ }
+ if ($isDup) {
+ Write-Host "[WARN] OrderItemAuto already exists in variant `"$varName`" — skipped"
+ continue
+ }
+ } else {
+ $existingOrd = Find-ElementByChildValue $orderEl "item" "field" $parsed.field $setNs
+ if ($existingOrd) {
+ Write-Host "[WARN] Order `"$($parsed.field)`" already exists in variant `"$varName`" — skipped"
+ continue
+ }
+ }
+
+ $fragXml = Build-OrderItemFragment -parsed $parsed -indent $orderIndent
+ $nodes = Import-Fragment $xmlDoc $fragXml
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $orderEl $node $null $orderIndent
+ }
+
+ $desc = if ($parsed.field -eq "Auto") { "Auto" } else { "$($parsed.field) $($parsed.direction)" }
+ Write-Host "[OK] Order `"$desc`" added to variant `"$varName`""
+ }
+ }
+
+ "add-selection" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+
+ foreach ($val in $values) {
+ $fieldName = $val.Trim()
+
+ $selection = Ensure-SettingsChild $settings "selection" @()
+ $selIndent = Get-ContainerChildIndent $selection
+
+ $selXml = Build-SelectionItemFragment -fieldName $fieldName -indent $selIndent
+ $selNodes = Import-Fragment $xmlDoc $selXml
+ foreach ($node in $selNodes) {
+ Insert-BeforeElement $selection $node $null $selIndent
+ }
+
+ Write-Host "[OK] Selection `"$fieldName`" added to variant `"$varName`""
+ }
+ }
+
+ "set-query" {
+ $dsNode = Resolve-DataSet
+ $dsName = Get-DataSetName $dsNode
+
+ $queryEl = Find-FirstElement $dsNode @("query") $schNs
+ if (-not $queryEl) {
+ Write-Error "No element found in dataset '$dsName'"
+ exit 1
+ }
+
+ # InnerText setter handles XML escaping automatically
+ $queryEl.InnerText = $Value
+
+ Write-Host "[OK] Query replaced in dataset `"$dsName`""
+ }
+
+ "set-outputParameter" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+
+ foreach ($val in $values) {
+ $parsed = Parse-OutputParamShorthand $val
+
+ $outputEl = Ensure-SettingsChild $settings "outputParameters" @("conditionalAppearance","order","filter","selection")
+ $outputIndent = Get-ContainerChildIndent $outputEl
+
+ # Remove existing parameter with same key if present
+ $existingParam = Find-ElementByChildValue $outputEl "item" "parameter" $parsed.key $corNs
+ if ($existingParam) {
+ Remove-NodeWithWhitespace $existingParam
+ Write-Host "[OK] Replaced outputParameter `"$($parsed.key)`" in variant `"$varName`""
+ } else {
+ Write-Host "[OK] OutputParameter `"$($parsed.key)`" added to variant `"$varName`""
+ }
+
+ $fragXml = Build-OutputParamFragment -parsed $parsed -indent $outputIndent
+ $nodes = Import-Fragment $xmlDoc $fragXml
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $outputEl $node $null $outputIndent
+ }
+ }
+ }
+
+ "set-structure" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+
+ # Remove all existing structure items (dcsset:item elements)
+ $toRemove = @()
+ foreach ($ch in $settings.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item' -and $ch.NamespaceURI -eq $setNs) {
+ $toRemove += $ch
+ }
+ }
+ foreach ($el in $toRemove) {
+ Remove-NodeWithWhitespace $el
+ }
+
+ # Parse structure shorthand
+ $structItems = Parse-StructureShorthand $Value
+ $settingsIndent = Get-ChildIndent $settings
+
+ # Find insertion point — before outputParameters/dataParameters/conditionalAppearance/order/filter/selection or at end
+ $refNode = Find-FirstElement $settings @("outputParameters","dataParameters","conditionalAppearance","order","filter","selection","item") $setNs
+ if (-not $refNode) { $refNode = $null }
+
+ foreach ($structItem in $structItems) {
+ $fragXml = Build-StructureItemFragment -item $structItem -indent $settingsIndent
+ $nodes = Import-Fragment $xmlDoc $fragXml
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $settings $node $refNode $settingsIndent
+ }
+ }
+
+ Write-Host "[OK] Structure set in variant `"$varName`": $Value"
+ }
+
+ "add-dataSetLink" {
+ foreach ($val in $values) {
+ $parsed = Parse-DataSetLinkShorthand $val
+ $root = $xmlDoc.DocumentElement
+ $childIndent = Get-ChildIndent $root
+
+ $fragXml = Build-DataSetLinkFragment -parsed $parsed -indent $childIndent
+ $nodes = Import-Fragment $xmlDoc $fragXml
+
+ # Insert after last dataSetLink, or before calculatedField/totalField/parameter/...
+ $lastLink = Find-LastElement $root "dataSetLink" $schNs
+ if ($lastLink) {
+ $refNode = $lastLink.NextSibling
+ while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) {
+ $refNode = $refNode.NextSibling
+ }
+ } else {
+ $refNode = Find-FirstElement $root @("calculatedField","totalField","parameter","template","groupTemplate","settingsVariant") $schNs
+ }
+
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $root $node $refNode $childIndent
+ }
+
+ $desc = "$($parsed.source) > $($parsed.dest) on $($parsed.sourceExpr) = $($parsed.destExpr)"
+ if ($parsed.parameter) { $desc += " [param $($parsed.parameter)]" }
+ Write-Host "[OK] DataSetLink `"$desc`" added"
+ }
+ }
+
+ "add-dataSet" {
+ $root = $xmlDoc.DocumentElement
+ $childIndent = Get-ChildIndent $root
+
+ $parsed = Parse-DataSetShorthand $Value
+
+ # Auto-name if empty
+ if (-not $parsed.name) {
+ $count = 0
+ foreach ($ch in $root.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'dataSet' -and $ch.NamespaceURI -eq $schNs) { $count++ }
+ }
+ $parsed.name = "НаборДанных$($count + 1)"
+ }
+
+ # Duplicate check
+ $existing = Find-ElementByChildValue $root "dataSet" "name" $parsed.name $schNs
+ if ($existing) {
+ Write-Host "[WARN] DataSet `"$($parsed.name)`" already exists — skipped"
+ } else {
+ # Get dataSource name from first existing
+ $dsSourceEl = Find-FirstElement $root @("dataSource") $schNs
+ $dsSourceName = "ИсточникДанных1"
+ if ($dsSourceEl) {
+ $nameEl = Find-FirstElement $dsSourceEl @("name") $schNs
+ if ($nameEl) { $dsSourceName = $nameEl.InnerText.Trim() }
+ }
+ $parsed["dataSource"] = $dsSourceName
+
+ $fragXml = Build-DataSetQueryFragment -parsed $parsed -indent $childIndent
+ $nodes = Import-Fragment $xmlDoc $fragXml
+
+ # Insert after last , or after if none
+ $lastDS = Find-LastElement $root "dataSet" $schNs
+ if ($lastDS) {
+ $refNode = $lastDS.NextSibling
+ while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) {
+ $refNode = $refNode.NextSibling
+ }
+ } else {
+ $refNode = Find-FirstElement $root @("dataSetLink","calculatedField","totalField","parameter","template","groupTemplate","settingsVariant") $schNs
+ }
+
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $root $node $refNode $childIndent
+ }
+
+ Write-Host "[OK] DataSet `"$($parsed.name)`" added (dataSource=$dsSourceName)"
+ }
+ }
+
+ "add-variant" {
+ $root = $xmlDoc.DocumentElement
+ $childIndent = Get-ChildIndent $root
+
+ foreach ($val in $values) {
+ $parsed = Parse-VariantShorthand $val
+
+ # Duplicate check — search for settingsVariant with matching dcsset:name
+ $isDup = $false
+ foreach ($ch in $root.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'settingsVariant' -and $ch.NamespaceURI -eq $schNs) {
+ foreach ($gc in $ch.ChildNodes) {
+ if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs -and $gc.InnerText -eq $parsed.name) {
+ $isDup = $true; break
+ }
+ }
+ if ($isDup) { break }
+ }
+ }
+ if ($isDup) {
+ Write-Host "[WARN] Variant `"$($parsed.name)`" already exists — skipped"
+ continue
+ }
+
+ $fragXml = Build-VariantFragment -parsed $parsed -indent $childIndent
+ $nodes = Import-Fragment $xmlDoc $fragXml
+
+ # Insert after last
+ $lastSV = Find-LastElement $root "settingsVariant" $schNs
+ if ($lastSV) {
+ $refNode = $lastSV.NextSibling
+ while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) {
+ $refNode = $refNode.NextSibling
+ }
+ } else {
+ $refNode = $null
+ }
+
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $root $node $refNode $childIndent
+ }
+
+ Write-Host "[OK] Variant `"$($parsed.name)`" [`"$($parsed.presentation)`"] added"
+ }
+ }
+
+ "add-conditionalAppearance" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+
+ foreach ($val in $values) {
+ $parsed = Parse-ConditionalAppearanceShorthand $val
+
+ $caEl = Ensure-SettingsChild $settings "conditionalAppearance" @("outputParameters","order","filter","selection")
+ $caIndent = Get-ContainerChildIndent $caEl
+
+ $fragXml = Build-ConditionalAppearanceItemFragment -parsed $parsed -indent $caIndent
+ $nodes = Import-Fragment $xmlDoc $fragXml
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $caEl $node $null $caIndent
+ }
+
+ $desc = "$($parsed.param) = $($parsed.value)"
+ if ($parsed.filter) { $desc += " when $($parsed.filter.field) $($parsed.filter.op)" }
+ if ($parsed.fields -and $parsed.fields.Count -gt 0) { $desc += " for $($parsed.fields -join ', ')" }
+ Write-Host "[OK] ConditionalAppearance `"$desc`" added to variant `"$varName`""
+ }
+ }
+
+ "clear-selection" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+ $selection = Find-FirstElement $settings @("selection") $setNs
+ if ($selection) {
+ Clear-ContainerChildren $selection
+ Write-Host "[OK] Selection cleared in variant `"$varName`""
+ } else {
+ Write-Host "[INFO] No selection section in variant `"$varName`""
+ }
+ }
+
+ "clear-order" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+ $orderEl = Find-FirstElement $settings @("order") $setNs
+ if ($orderEl) {
+ Clear-ContainerChildren $orderEl
+ Write-Host "[OK] Order cleared in variant `"$varName`""
+ } else {
+ Write-Host "[INFO] No order section in variant `"$varName`""
+ }
+ }
+
+ "clear-filter" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+ $filterEl = Find-FirstElement $settings @("filter") $setNs
+ if ($filterEl) {
+ Clear-ContainerChildren $filterEl
+ Write-Host "[OK] Filter cleared in variant `"$varName`""
+ } else {
+ Write-Host "[INFO] No filter section in variant `"$varName`""
+ }
+ }
+
+ "modify-filter" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+
+ foreach ($val in $values) {
+ $parsed = Parse-FilterShorthand $val
+
+ $filterEl = Find-FirstElement $settings @("filter") $setNs
+ if (-not $filterEl) {
+ Write-Host "[WARN] No filter section in variant `"$varName`""
+ continue
+ }
+
+ $filterItem = Find-ElementByChildValue $filterEl "item" "left" $parsed.field $setNs
+ if (-not $filterItem) {
+ Write-Host "[WARN] Filter for `"$($parsed.field)`" not found in variant `"$varName`""
+ continue
+ }
+
+ $itemIndent = Get-ChildIndent $filterItem
+
+ # Update comparisonType
+ Set-OrCreateChildElement $filterItem "comparisonType" $setNs $parsed.op $itemIndent
+
+ # Update right value
+ if ($null -ne $parsed.value) {
+ $vt = if ($parsed["valueType"]) { $parsed["valueType"] } else { "xs:string" }
+ Set-OrCreateChildElementWithAttr $filterItem "right" $setNs "$($parsed.value)" $vt $itemIndent
+ }
+
+ # Update use
+ if ($parsed.use -eq $false) {
+ Set-OrCreateChildElement $filterItem "use" $setNs "false" $itemIndent
+ } else {
+ # If explicitly not @off, remove use=false if exists
+ $useEl = $null
+ foreach ($ch in $filterItem.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $setNs) {
+ $useEl = $ch; break
+ }
+ }
+ if ($useEl -and $useEl.InnerText -eq 'false') {
+ Remove-NodeWithWhitespace $useEl
+ }
+ }
+
+ # Update viewMode
+ if ($parsed.viewMode) {
+ Set-OrCreateChildElement $filterItem "viewMode" $setNs $parsed.viewMode $itemIndent
+ }
+
+ # Update userSettingID
+ if ($parsed.userSettingID) {
+ $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID }
+ Set-OrCreateChildElement $filterItem "userSettingID" $setNs $uid $itemIndent
+ }
+
+ Write-Host "[OK] Filter `"$($parsed.field)`" modified in variant `"$varName`""
+ }
+ }
+
+ "modify-dataParameter" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+
+ foreach ($val in $values) {
+ $parsed = Parse-DataParamShorthand $val
+
+ $dpEl = Find-FirstElement $settings @("dataParameters") $setNs
+ if (-not $dpEl) {
+ Write-Host "[WARN] No dataParameters section in variant `"$varName`""
+ continue
+ }
+
+ $dpItem = Find-ElementByChildValue $dpEl "item" "parameter" $parsed.parameter $corNs
+ if (-not $dpItem) {
+ Write-Host "[WARN] DataParameter `"$($parsed.parameter)`" not found in variant `"$varName`""
+ continue
+ }
+
+ $itemIndent = Get-ChildIndent $dpItem
+
+ # Update value
+ if ($null -ne $parsed.value) {
+ # Remove existing value element first
+ $existingVal = $null
+ foreach ($ch in $dpItem.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'value' -and $ch.NamespaceURI -eq $corNs) {
+ $existingVal = $ch; break
+ }
+ }
+ if ($existingVal) {
+ Remove-NodeWithWhitespace $existingVal
+ }
+
+ # Build new value fragment
+ $valLines = @()
+ if ($parsed.value -is [hashtable] -and $parsed.value.variant) {
+ $valLines += "$itemIndent"
+ $valLines += "$itemIndent`t$(Esc-Xml $parsed.value.variant)"
+ $valLines += "$itemIndent"
+ } elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') {
+ $valLines += "$itemIndent$(Esc-Xml "$($parsed.value)")"
+ } elseif ("$($parsed.value)" -eq "true" -or "$($parsed.value)" -eq "false") {
+ $valLines += "$itemIndent$(Esc-Xml "$($parsed.value)")"
+ } else {
+ $valLines += "$itemIndent$(Esc-Xml "$($parsed.value)")"
+ }
+ $valXml = $valLines -join "`r`n"
+ $valNodes = Import-Fragment $xmlDoc $valXml
+ foreach ($node in $valNodes) {
+ Insert-BeforeElement $dpItem $node $null $itemIndent
+ }
+ }
+
+ # Update use
+ if ($parsed.use -eq $false) {
+ Set-OrCreateChildElement $dpItem "use" $corNs "false" $itemIndent
+ } else {
+ $useEl = $null
+ foreach ($ch in $dpItem.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $corNs) {
+ $useEl = $ch; break
+ }
+ }
+ if ($useEl -and $useEl.InnerText -eq 'false') {
+ Remove-NodeWithWhitespace $useEl
+ }
+ }
+
+ # Update viewMode
+ if ($parsed.viewMode) {
+ Set-OrCreateChildElement $dpItem "viewMode" $setNs $parsed.viewMode $itemIndent
+ }
+
+ # Update userSettingID
+ if ($parsed.userSettingID) {
+ $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID }
+ Set-OrCreateChildElement $dpItem "userSettingID" $setNs $uid $itemIndent
+ }
+
+ Write-Host "[OK] DataParameter `"$($parsed.parameter)`" modified in variant `"$varName`""
+ }
+ }
+
+ "modify-field" {
+ $dsNode = Resolve-DataSet
+ $dsName = Get-DataSetName $dsNode
+
+ foreach ($val in $values) {
+ $parsed = Parse-FieldShorthand $val
+ $fieldName = $parsed.dataPath
+
+ # Find existing field
+ $fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $fieldName $schNs
+ if (-not $fieldEl) {
+ Write-Host "[WARN] Field `"$fieldName`" not found in dataset `"$dsName`""
+ continue
+ }
+
+ # Read existing properties
+ $existing = Read-FieldProperties $fieldEl
+
+ # Merge: parsed overrides existing for non-empty values
+ $merged = @{
+ dataPath = $existing.dataPath
+ field = $existing.field
+ title = if ($parsed.title) { $parsed.title } else { $existing.title }
+ type = if ($parsed.type) { $parsed.type } else { $existing.type }
+ roles = if ($parsed.roles -and $parsed.roles.Count -gt 0) { $parsed.roles } else { $existing.roles }
+ restrict = if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { $parsed.restrict } else { $existing.restrict }
+ }
+
+ # Remember position (NextSibling after whitespace)
+ $nextSib = $fieldEl.NextSibling
+ while ($nextSib -and ($nextSib.NodeType -eq 'Whitespace' -or $nextSib.NodeType -eq 'SignificantWhitespace')) {
+ $nextSib = $nextSib.NextSibling
+ }
+
+ # Remove old field
+ $childIndent = Get-ChildIndent $dsNode
+ Remove-NodeWithWhitespace $fieldEl
+
+ # Build new field fragment with merged data
+ $fragXml = Build-FieldFragment -parsed $merged -indent $childIndent
+ $nodes = Import-Fragment $xmlDoc $fragXml
+
+ # Insert at saved position
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $dsNode $node $nextSib $childIndent
+ }
+
+ Write-Host "[OK] Field `"$fieldName`" modified in dataset `"$dsName`""
+ }
+ }
+
+ "remove-field" {
+ $dsNode = Resolve-DataSet
+ $dsName = Get-DataSetName $dsNode
+
+ foreach ($val in $values) {
+ $fieldName = $val.Trim()
+
+ $fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $fieldName $schNs
+ if (-not $fieldEl) {
+ Write-Host "[WARN] Field `"$fieldName`" not found in dataset `"$dsName`""
+ continue
+ }
+
+ Remove-NodeWithWhitespace $fieldEl
+ Write-Host "[OK] Field `"$fieldName`" removed from dataset `"$dsName`""
+
+ # Also remove from selection in variant
+ try {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+ $selection = Find-FirstElement $settings @("selection") $setNs
+ if ($selection) {
+ $selItem = Find-ElementByChildValue $selection "item" "field" $fieldName $setNs
+ if ($selItem) {
+ Remove-NodeWithWhitespace $selItem
+ Write-Host "[OK] Field `"$fieldName`" removed from selection of variant `"$varName`""
+ }
+ }
+ } catch {
+ # No variant — that's fine
+ }
+ }
+ }
+
+ "remove-total" {
+ foreach ($val in $values) {
+ $dataPath = $val.Trim()
+ $root = $xmlDoc.DocumentElement
+
+ $totalEl = Find-ElementByChildValue $root "totalField" "dataPath" $dataPath $schNs
+ if (-not $totalEl) {
+ Write-Host "[WARN] TotalField `"$dataPath`" not found"
+ continue
+ }
+
+ Remove-NodeWithWhitespace $totalEl
+ Write-Host "[OK] TotalField `"$dataPath`" removed"
+ }
+ }
+
+ "remove-calculated-field" {
+ foreach ($val in $values) {
+ $dataPath = $val.Trim()
+ $root = $xmlDoc.DocumentElement
+
+ $calcEl = Find-ElementByChildValue $root "calculatedField" "dataPath" $dataPath $schNs
+ if (-not $calcEl) {
+ Write-Host "[WARN] CalculatedField `"$dataPath`" not found"
+ continue
+ }
+
+ Remove-NodeWithWhitespace $calcEl
+ Write-Host "[OK] CalculatedField `"$dataPath`" removed"
+
+ # Also remove from selection
+ try {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+ $selection = Find-FirstElement $settings @("selection") $setNs
+ if ($selection) {
+ $selItem = Find-ElementByChildValue $selection "item" "field" $dataPath $setNs
+ if ($selItem) {
+ Remove-NodeWithWhitespace $selItem
+ Write-Host "[OK] Field `"$dataPath`" removed from selection of variant `"$varName`""
+ }
+ }
+ } catch { }
+ }
+ }
+
+ "remove-parameter" {
+ foreach ($val in $values) {
+ $paramName = $val.Trim()
+ $root = $xmlDoc.DocumentElement
+
+ $paramEl = Find-ElementByChildValue $root "parameter" "name" $paramName $schNs
+ if (-not $paramEl) {
+ Write-Host "[WARN] Parameter `"$paramName`" not found"
+ continue
+ }
+
+ Remove-NodeWithWhitespace $paramEl
+ Write-Host "[OK] Parameter `"$paramName`" removed"
+ }
+ }
+
+ "remove-filter" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+
+ foreach ($val in $values) {
+ $fieldName = $val.Trim()
+
+ $filterEl = Find-FirstElement $settings @("filter") $setNs
+ if (-not $filterEl) {
+ Write-Host "[WARN] No filter section in variant `"$varName`""
+ continue
+ }
+
+ $filterItem = Find-ElementByChildValue $filterEl "item" "left" $fieldName $setNs
+ if (-not $filterItem) {
+ Write-Host "[WARN] Filter for `"$fieldName`" not found in variant `"$varName`""
+ continue
+ }
+
+ Remove-NodeWithWhitespace $filterItem
+ Write-Host "[OK] Filter for `"$fieldName`" removed from variant `"$varName`""
+ }
+ }
+}
+
+# --- 9. Save ---
+
+$content = $xmlDoc.OuterXml
+$content = $content -replace '(?<=<\?xml[^?]*encoding=")utf-8(?=")', 'UTF-8'
+$enc = New-Object System.Text.UTF8Encoding($true)
+[System.IO.File]::WriteAllText($resolvedPath, $content, $enc)
+
+Write-Host "[OK] Saved $resolvedPath"
diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md
new file mode 100644
index 00000000..4acc6019
--- /dev/null
+++ b/.claude/skills/skd-info/SKILL.md
@@ -0,0 +1,74 @@
+---
+name: skd-info
+description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты
+argument-hint: [-Mode overview|query|fields|links|calculated|resources|params|variant|templates|trace] [-Name ]
+allowed-tools:
+ - Bash
+ - Read
+ - Glob
+---
+
+# /skd-info — Анализ схемы компоновки данных
+
+Читает Template.xml схемы компоновки данных (СКД) и выводит компактную сводку. Заменяет необходимость читать тысячи строк XML.
+
+## Параметры и команда
+
+| Параметр | Описание |
+|----------|----------|
+| `TemplatePath` | Путь к Template.xml или каталогу макета (авто-резолв в `Ext/Template.xml`) |
+| `Mode` | Режим анализа (по умолчанию `overview`) |
+| `Name` | Имя набора (query), поля (fields/calculated/resources/trace), варианта (variant) или группировки/поля (templates) |
+| `Batch` | Номер пакета запроса, 0 = все (только query) |
+| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
+| `OutFile` | Записать результат в файл (UTF-8 BOM) |
+
+```powershell
+powershell.exe -NoProfile -File .claude\skills\skd-info\scripts\skd-info.ps1 -TemplatePath "<путь>"
+```
+
+С указанием режима:
+```powershell
+... -Mode query -Name НоменклатураСЦенами
+... -Mode query -Name ДанныеТ13 -Batch 3
+... -Mode fields -Name КадастроваяСтоимость
+... -Mode calculated -Name КоэффициентКи
+... -Mode resources -Name СуммаНалога
+... -Mode trace -Name "Коэффициент Ки"
+... -Mode variant -Name 1
+... -Mode templates
+... -Mode templates -Name ВидНалоговойБазы
+```
+
+## Режимы
+
+| Режим | Без `-Name` | С `-Name` |
+|-------|-------------|-----------|
+| `overview` | Навигационная карта схемы + подсказки Next | — |
+| `query` | — | Текст запроса набора (с оглавлением батчей) |
+| `fields` | Карта: имена полей по наборам | Деталь поля: набор, тип, роль, формат |
+| `links` | Все связи наборов | — |
+| `calculated` | Карта: имена вычисляемых полей | Выражение + заголовок + ограничения |
+| `resources` | Карта: имена ресурсов (`*` = групповые формулы) | Формулы агрегации по группировкам |
+| `params` | Таблица параметров: тип, значение, видимость | — |
+| `variant` | Список вариантов | Структура группировок + фильтры + вывод |
+| `templates` | Карта привязок шаблонов (field/group) | Содержимое шаблона: строки, ячейки, выражения |
+| `trace` | — | Полная цепочка: набор → вычисление → ресурс |
+
+Паттерн: без `-Name` — карта/индекс, с `-Name` — деталь конкретного элемента.
+
+## Типичный workflow
+
+1. `overview` — понять структуру, увидеть подсказки
+2. `trace -Name <поле>` — узнать как считается колонка отчёта (от заголовка до запроса за один вызов)
+3. `query -Name <набор>` — посмотреть текст SQL-запроса
+4. `variant -Name ` — посмотреть группировки и фильтры варианта
+
+Подробные примеры вывода каждого режима — в `modes-reference.md`.
+
+## Верификация
+
+```
+/skd-info — overview (точка входа)
+/skd-info -Mode trace -Name — трассировка поля
+```
diff --git a/.claude/skills/skd-info/modes-reference.md b/.claude/skills/skd-info/modes-reference.md
new file mode 100644
index 00000000..eb6bd983
--- /dev/null
+++ b/.claude/skills/skd-info/modes-reference.md
@@ -0,0 +1,246 @@
+# /skd-info — полная справка по режимам
+
+Компактное описание — в [SKILL.md](SKILL.md).
+
+## overview (по умолчанию) — карта схемы
+
+Компактная навигационная карта (10-25 строк). Показывает структуру и подсказывает следующие шаги:
+
+```
+=== DCS: ОсновнаяСхемаКомпоновкиДанных (362 lines) ===
+
+Sources: ИсточникДанных1 (Local)
+
+Datasets:
+ [Query] НоменклатураСЦенами 7 fields, query 40 lines
+Calculated: 1
+Resources: 1
+Templates: 1 templates, 1 group bindings
+Params: (none)
+
+Variants:
+ [1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
+ [2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
+
+Next:
+ -Mode query query text
+ -Mode fields field tables by dataset
+ -Mode calculated calculated field expressions
+ -Mode resources resource aggregation
+ -Mode variant -Name variant structure (1..2)
+```
+
+Для DataSetUnion — дерево наборов + связи:
+```
+Datasets:
+ [Union] РасчетНалогаНаИмущество 52 fields
+ ├─ [Query] РасчетНалогаНаИмущество 51 fields, query 181 lines
+ ├─ [Query] ДанныеПоКадастровой 29 fields, query 40 lines
+ ├─ [Query] ДанныеПоСреднегодовой 34 fields, query 41 lines
+Links: РасчетНалогаНаИмущество -> СостояниеОС (2 fields)
+```
+
+Параметры разделяются на видимые/скрытые:
+```
+Params: 18 (7 visible, 11 hidden): Период, Ответственный, ...
+```
+
+## query — текст запроса
+
+`-Name <набор>` — имя DataSet (обязателен если наборов > 1).
+
+Извлекает raw-текст запроса с деэкранированием XML (`&`→`&`, `>`→`>`). Для пакетных запросов — оглавление батчей:
+
+```
+=== Query: ДанныеТ13 (334 lines, 13 batches) ===
+ Batch 1: lines 1-8 → ПОМЕСТИТЬ Представления_Периоды
+ Batch 2: lines 9-26 → ПОМЕСТИТЬ Представления_СотрудникиОрганизации
+ ...
+--- Batch 1 ---
+ВЫБРАТЬ
+ ДАТАВРЕМЯ(1, 1, 1) КАК Период
+ПОМЕСТИТЬ Представления_Периоды
+...
+```
+
+Фильтр по номеру батча: `-Batch 3` покажет только 3-й пакет.
+
+## fields — поля наборов данных
+
+Без `-Name` — карта: имена полей по наборам:
+```
+=== Fields map ===
+СостояниеОС [Query] (3): Организация, ОсновноеСредство, ДатаСостояния
+РасчетНалогаНаИмущество [Union] (52): ДоляСтоимостиЧислитель, ...
+ РасчетНалогаНаИмущество [Query] (51): КадастроваяСтоимость, ...
+```
+
+С `-Name <поле>` — детали конкретного поля:
+```
+=== Field: ДатаСостояния "Дата ввода в эксплуатацию" ===
+
+Dataset: СостояниеОС [Query]
+Format: ДФ=dd.MM.yyyy
+```
+
+Показывает: dataset, title, type, role, useRestriction, format, presentationExpression.
+
+## links — связи наборов данных
+
+```
+=== Links (4) ===
+
+РасчетНалогаНаИмущество -> СостояниеОС :
+ Организация -> Организация
+ ОсновноеСредство -> ОсновноеСредство
+```
+
+Группирует по парам наборов. Показывает поля связи и параметры.
+
+## calculated — вычисляемые поля
+
+Без `-Name` — карта: имена и заголовки:
+```
+=== Calculated fields (23) ===
+ ДоляСтоимости "Доля стоимости"
+ КоэффициентКи "Коэффициент Ки"
+ ...
+```
+
+С `-Name <поле>` — полное выражение:
+```
+=== Calculated: ДоляСтоимости ===
+
+Expression:
+ ВЫБОР КОГДА ... ТОГДА "1" ИНАЧЕ ... КОНЕЦ
+Title: Доля стоимости
+Restrict: condition
+```
+
+## resources — ресурсы (итоги по группировкам)
+
+Без `-Name` — карта: имена полей, `*` = есть формулы по группировкам:
+```
+=== Resources (51) ===
+ НалоговаяБаза
+ КоэффициентКи *
+ ...
+ * = has group-level formulas
+```
+
+С `-Name <поле>` — формулы агрегации:
+```
+=== Resource: ДатаСостояния ===
+
+ [ОсновноеСредство] ЕстьNull(ДатаСостояния, "")
+```
+
+## params — параметры схемы
+
+```
+=== Parameters (16) ===
+ Name Type Default Visible Expression
+ Период StandardPeriod LastMonth yes -
+ НачалоПериода DateTime - hidden &Период.ДатаНачала
+ Организация CatalogRef.Организации null yes -
+```
+
+## variant — варианты отчёта
+
+Без `-Name` — список вариантов:
+```
+=== Variants (2) ===
+ [1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
+ [2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
+```
+
+С `-Name ` — структура конкретного варианта:
+```
+=== Variant [1]: НоменклатураИЦены "Номенклатура и цены" ===
+
+Structure:
+ Table "Таблица"
+ ├── Columns: [ТипЦен Items]
+ │ Selection: Auto, Цена
+ └── Rows: [Номенклатура Items]
+ Selection: Номенклатура, УИД, Auto
+
+Filter:
+ [ ] Номенклатура InHierarchy [user]
+ [ ] ТипЦен Equal
+ [x] ВАрхиве = false "Исключая скрытые товары"
+
+DataParams: КлючВарианта="НоменклатураИЦены"
+Output: style=ЧерноБелый groups=Separately totalsH=None totalsV=None
+```
+
+## templates — привязки шаблонов вывода
+
+Три типа привязок: `fieldTemplate` (к полю), `groupTemplate` (к группировке, Header/Footer), `groupHeaderTemplate` (заголовок группы).
+
+Без `-Name` — карта привязок:
+```
+=== Templates (70 defined: 49 field, 37 group) ===
+
+Field bindings (49): (all trivial)
+ ОстаточнаяСтоимостьНа0101, ОстаточнаяСтоимостьНа0102, ...
+
+Group bindings (37):
+ ВидНалоговойБазы
+ Header -> Макет3 (1 rows, 1 params)
+ СреднегодоваяСтоимость2019
+ Footer -> Макет50 (1 rows) spacer
+ GroupHeader -> Макет40 (3 rows)
+```
+
+С `-Name <группировка|поле>` — содержимое шаблонов:
+```
+=== Templates: СреднегодоваяСтоимость2019 ===
+
+Footer -> Макет50 [1 rows, 1 cells]:
+ Row 1: (empty)
+
+GroupHeader -> Макет40 [3 rows, 78 cells]:
+ Row 1: "№ п/п" | "###Группировки1###" | "Инв. номер" | ...
+ Row 2: "01.01" | "01.02" | ... | "31.12"
+ Row 3: "1" | "2" | ... | "26"
+```
+
+Для field-привязок:
+```
+=== Field template: ОстаточнаяСтоимостьНа0101 -> Макет4 ===
+[1 rows, 1 cells]
+ Row 1: {ОстаточнаяСтоимостьНа0101}
+ (all params trivial)
+```
+
+**Тривиальность выражений**: `Поле = Поле` и `Поле = Представление(Поле)` считаются тривиальными и НЕ выводятся. Показываются только нетривиальные — когда выражение содержит другое поле, вызов метода, пустую строку и т.д.
+
+## trace — трассировка поля от заголовка до запроса
+
+Ищет поле по dataPath ИЛИ заголовку (включая подстроку) и показывает полную цепочку происхождения за один вызов:
+
+```
+=== Trace: КоэффициентКи "Коэффициент Ки" ===
+
+Dataset: (schema-level only, not in dataset fields)
+
+Calculated:
+ ВЫБОР КОГДА ... ТОГДА 0 ИНАЧЕ ... КОНЕЦ
+ Operands:
+ КоличествоМесяцевИспользования -> РасчетНалогаНаИмущество [Query]
+ КоличествоМесяцевВладения -> РасчетНалогаНаИмущество [Query]
+
+Resource:
+ [ОсновноеСредство] Сумма(КоэффициентКи)
+```
+
+Типичный сценарий: пользователь видит колонку "Коэффициент Ки" в отчёте и спрашивает как она считается. Один вызов `trace` показывает: формулу вычисления, откуда берутся операнды, как агрегируется в ресурс.
+
+## Что не выводится
+
+- XML namespace-декларации
+- Обёртки v8:item/v8:lang/v8:content (извлекаем чистый текст)
+- userSettingID (GUID-ы пользовательских настроек)
+- Дефолтные periodAdditionBegin/End = 0001-01-01
+- viewMode
diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1
new file mode 100644
index 00000000..6dfc8a60
--- /dev/null
+++ b/.claude/skills/skd-info/scripts/skd-info.ps1
@@ -0,0 +1,1792 @@
+# skd-info v1.0 — Analyze 1C DCS structure
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+param(
+ [Parameter(Mandatory=$true)]
+ [string]$TemplatePath,
+ [ValidateSet("overview", "query", "fields", "links", "calculated", "resources", "params", "variant", "trace", "templates")]
+ [string]$Mode = "overview",
+ [string]$Name,
+ [int]$Batch = 0,
+ [int]$Limit = 150,
+ [int]$Offset = 0,
+ [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
+
+# --- Load XML ---
+
+$xmlDoc = New-Object System.Xml.XmlDocument
+$xmlDoc.PreserveWhitespace = $false
+$xmlDoc.Load($resolvedPath)
+
+$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
+
+# --- Helpers ---
+
+function Get-MLText($node) {
+ if (-not $node) { return "" }
+ $content = $node.SelectSingleNode("v8:item/v8:content", $ns)
+ if ($content) { return $content.InnerText }
+ $text = $node.InnerText.Trim()
+ if ($text) { return $text }
+ return ""
+}
+
+function Unescape-Xml([string]$text) {
+ if (-not $text) { return $text }
+ $text = $text.Replace("&", "&")
+ $text = $text.Replace(">", ">")
+ $text = $text.Replace("<", "<")
+ $text = $text.Replace(""", '"')
+ $text = $text.Replace("'", "'")
+ return $text
+}
+
+function Get-CompactType($valueTypeNode) {
+ if (-not $valueTypeNode) { return "" }
+ $types = @()
+ foreach ($t in $valueTypeNode.SelectNodes("v8:Type", $ns)) {
+ $raw = $t.InnerText
+ switch -Wildcard ($raw) {
+ "xs:string" { $types += "String" }
+ "xs:decimal" { $types += "Number" }
+ "xs:boolean" { $types += "Boolean" }
+ "xs:dateTime" { $types += "DateTime" }
+ "v8:StandardPeriod" { $types += "StandardPeriod" }
+ "v8:StandardBeginningDate" { $types += "StandardBeginningDate" }
+ "v8:AccountType" { $types += "AccountType" }
+ "v8:Null" { $types += "Null" }
+ default {
+ # Strip namespace prefixes like d4p1: cfg:
+ $clean = $raw -replace '^[a-zA-Z0-9]+:', ''
+ $types += $clean
+ }
+ }
+ }
+ if ($types.Count -eq 0) { return "" }
+ return ($types -join " | ")
+}
+
+function Get-DataSetType($dsNode) {
+ $xsiType = $dsNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
+ if ($xsiType -like "*DataSetQuery*") { return "Query" }
+ if ($xsiType -like "*DataSetObject*") { return "Object" }
+ if ($xsiType -like "*DataSetUnion*") { return "Union" }
+ return "Unknown"
+}
+
+function Get-FieldCount($dsNode) {
+ return $dsNode.SelectNodes("s:field", $ns).Count
+}
+
+function Get-QueryLineCount($dsNode) {
+ $queryNode = $dsNode.SelectSingleNode("s:query", $ns)
+ if (-not $queryNode) { return 0 }
+ $text = $queryNode.InnerText
+ return ($text -split "`n").Count
+}
+
+function Get-StructureItemType($itemNode) {
+ $xsiType = $itemNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
+ if ($xsiType -like "*StructureItemGroup*") { return "Group" }
+ if ($xsiType -like "*StructureItemTable*") { return "Table" }
+ if ($xsiType -like "*StructureItemChart*") { return "Chart" }
+ return "Unknown"
+}
+
+function Get-GroupFields($itemNode) {
+ $fields = @()
+ foreach ($gi in $itemNode.SelectNodes("dcsset:groupItems/dcsset:item", $ns)) {
+ $fieldNode = $gi.SelectSingleNode("dcsset:field", $ns)
+ $groupType = $gi.SelectSingleNode("dcsset:groupType", $ns)
+ if ($fieldNode) {
+ $f = $fieldNode.InnerText
+ $gt = if ($groupType) { $groupType.InnerText } else { "" }
+ if ($gt -and $gt -ne "Items") { $f += "($gt)" }
+ $fields += $f
+ }
+ }
+ return $fields
+}
+
+function Get-SelectionFields($itemNode) {
+ $fields = @()
+ foreach ($si in $itemNode.SelectNodes("dcsset:selection/dcsset:item", $ns)) {
+ $xsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
+ if ($xsiType -like "*SelectedItemAuto*") {
+ $fields += "Auto"
+ } elseif ($xsiType -like "*SelectedItemField*") {
+ $f = $si.SelectSingleNode("dcsset:field", $ns)
+ if ($f) { $fields += $f.InnerText }
+ } elseif ($xsiType -like "*SelectedItemFolder*") {
+ $fields += "Folder"
+ }
+ }
+ return $fields
+}
+
+function Get-FilterSummary($settingsNode) {
+ $filters = @()
+ foreach ($fi in $settingsNode.SelectNodes("dcsset:filter/dcsset:item", $ns)) {
+ $xsiType = $fi.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
+
+ if ($xsiType -like "*FilterItemGroup*") {
+ $groupType = $fi.SelectSingleNode("dcsset:groupType", $ns)
+ $gt = if ($groupType) { $groupType.InnerText } else { "And" }
+ $subCount = $fi.SelectNodes("dcsset:item", $ns).Count
+ $filters += "[Group:$gt $subCount items]"
+ continue
+ }
+
+ $use = $fi.SelectSingleNode("dcsset:use", $ns)
+ $isActive = if ($use -and $use.InnerText -eq "false") { "[ ]" } else { "[x]" }
+
+ $left = $fi.SelectSingleNode("dcsset:left", $ns)
+ $comp = $fi.SelectSingleNode("dcsset:comparisonType", $ns)
+ $right = $fi.SelectSingleNode("dcsset:right", $ns)
+ $pres = $fi.SelectSingleNode("dcsset:presentation", $ns)
+ $userSetting = $fi.SelectSingleNode("dcsset:userSettingID", $ns)
+
+ $leftStr = if ($left) { $left.InnerText } else { "?" }
+ $compStr = if ($comp) { $comp.InnerText } else { "?" }
+ $rightStr = ""
+ if ($right) {
+ $rightStr = " $($right.InnerText)"
+ }
+
+ $presStr = ""
+ if ($pres) {
+ $pt = Get-MLText $pres
+ if ($pt) { $presStr = " `"$pt`"" }
+ }
+
+ $userStr = ""
+ if ($userSetting) { $userStr = " [user]" }
+
+ $filters += "$isActive $leftStr $compStr$rightStr$presStr$userStr"
+ }
+ return $filters
+}
+
+function Build-StructureTree {
+ param($itemNode, [string]$prefix, [bool]$isLast, [System.Collections.Generic.List[string]]$outLines)
+
+ $itemType = Get-StructureItemType $itemNode
+ $nameNode = $itemNode.SelectSingleNode("dcsset:name", $ns)
+ $itemName = if ($nameNode) { $nameNode.InnerText } else { "" }
+
+ $groupFields = Get-GroupFields $itemNode
+ $groupStr = if ($groupFields.Count -gt 0) { "[" + ($groupFields -join ", ") + "]" } else { "(detail)" }
+
+ $selFields = Get-SelectionFields $itemNode
+ $selStr = if ($selFields.Count -gt 0) { "Selection: " + ($selFields -join ", ") } else { "" }
+
+ $line = ""
+ switch ($itemType) {
+ "Group" {
+ $line = "$itemType $groupStr"
+ if ($itemName) { $line = "$itemType `"$itemName`" $groupStr" }
+ }
+ "Table" {
+ $line = "Table"
+ if ($itemName) { $line = "Table `"$itemName`"" }
+ }
+ "Chart" {
+ $line = "Chart"
+ if ($itemName) { $line = "Chart `"$itemName`"" }
+ }
+ }
+
+ $outLines.Add("$prefix$line")
+ if ($selStr -and $itemType -eq "Group") {
+ $outLines.Add("$prefix $selStr")
+ }
+
+ # For Table, show columns and rows
+ if ($itemType -eq "Table") {
+ $columns = $itemNode.SelectNodes("dcsset:column", $ns)
+ $rows = $itemNode.SelectNodes("dcsset:row", $ns)
+
+ foreach ($col in $columns) {
+ $colGroup = Get-GroupFields $col
+ $colGroupStr = if ($colGroup.Count -gt 0) { "[" + ($colGroup -join ", ") + "]" } else { "(detail)" }
+ $colSel = Get-SelectionFields $col
+ $colSelStr = if ($colSel.Count -gt 0) { "Selection: " + ($colSel -join ", ") } else { "" }
+ $connC = if ($rows.Count -gt 0) { [string][char]0x251C + [string][char]0x2500 + [string][char]0x2500 } else { [string][char]0x2514 + [string][char]0x2500 + [string][char]0x2500 }
+ $contC = if ($rows.Count -gt 0) { [string][char]0x2502 + " " } else { " " }
+ $outLines.Add("$prefix$connC Columns: $colGroupStr")
+ if ($colSelStr) { $outLines.Add("$prefix$contC $colSelStr") }
+ }
+
+ foreach ($row in $rows) {
+ $rowGroup = Get-GroupFields $row
+ $rowGroupStr = if ($rowGroup.Count -gt 0) { "[" + ($rowGroup -join ", ") + "]" } else { "(detail)" }
+ $rowSel = Get-SelectionFields $row
+ $rowSelStr = if ($rowSel.Count -gt 0) { "Selection: " + ($rowSel -join ", ") } else { "" }
+ $outLines.Add("$prefix" + [string][char]0x2514 + [string][char]0x2500 + [string][char]0x2500 + " Rows: $rowGroupStr")
+ if ($rowSelStr) { $outLines.Add("$prefix $rowSelStr") }
+ }
+ }
+
+ # Recurse into nested structure items (for Group)
+ if ($itemType -eq "Group") {
+ $children = $itemNode.SelectNodes("dcsset:item", $ns)
+ for ($i = 0; $i -lt $children.Count; $i++) {
+ $child = $children[$i]
+ $childType = $child.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
+ if ($childType -like "*StructureItem*") {
+ $last = ($i -eq $children.Count - 1)
+ $connector = if ($last) { [string][char]0x2514 + [string][char]0x2500 + " " } else { [string][char]0x251C + [string][char]0x2500 + " " }
+ $continuation = if ($last) { " " } else { [string][char]0x2502 + " " }
+ Build-StructureTree -itemNode $child -prefix "$prefix$continuation" -isLast $last -outLines $outLines
+ }
+ }
+ }
+}
+
+# --- Output collector ---
+
+$lines = [System.Collections.Generic.List[string]]::new()
+
+# Determine template name from path
+$pathParts = $resolvedPath -split '[/\\]'
+$templateName = $resolvedPath
+for ($i = $pathParts.Count - 1; $i -ge 0; $i--) {
+ if ($pathParts[$i] -eq "Ext" -and $i -ge 1) {
+ $templateName = $pathParts[$i - 1]
+ break
+ }
+}
+
+$totalXmlLines = (Get-Content $resolvedPath).Count
+
+# ============================================================
+# MODE: overview
+# ============================================================
+if ($Mode -eq "overview") {
+
+ $lines.Add("=== DCS: $templateName ($totalXmlLines lines) ===")
+ $lines.Add("")
+
+ # Sources
+ $sources = @()
+ foreach ($ds in $root.SelectNodes("s:dataSource", $ns)) {
+ $dsName = $ds.SelectSingleNode("s:name", $ns).InnerText
+ $dsType = $ds.SelectSingleNode("s:dataSourceType", $ns).InnerText
+ $sources += "$dsName ($dsType)"
+ }
+ $lines.Add("Sources: " + ($sources -join ", "))
+ $lines.Add("")
+
+ # Datasets (recursive for Union)
+ $lines.Add("Datasets:")
+ foreach ($ds in $root.SelectNodes("s:dataSet", $ns)) {
+ $dsType = Get-DataSetType $ds
+ $dsName = $ds.SelectSingleNode("s:name", $ns).InnerText
+ $fieldCount = Get-FieldCount $ds
+
+ switch ($dsType) {
+ "Query" {
+ $queryLines = Get-QueryLineCount $ds
+ $lines.Add(" [Query] $dsName $fieldCount fields, query $queryLines lines")
+ }
+ "Object" {
+ $objName = $ds.SelectSingleNode("s:objectName", $ns)
+ $objStr = if ($objName) { " objectName=$($objName.InnerText)" } else { "" }
+ $lines.Add(" [Object] $dsName$objStr $fieldCount fields")
+ }
+ "Union" {
+ $lines.Add(" [Union] $dsName $fieldCount fields")
+ foreach ($subDs in $ds.SelectNodes("s:item", $ns)) {
+ $subType = Get-DataSetType $subDs
+ $subName = $subDs.SelectSingleNode("s:name", $ns)
+ $subNameStr = if ($subName) { $subName.InnerText } else { "?" }
+ $subFields = Get-FieldCount $subDs
+ switch ($subType) {
+ "Query" {
+ $subQueryLines = Get-QueryLineCount $subDs
+ $lines.Add(" " + [string][char]0x251C + [string][char]0x2500 + " [Query] $subNameStr $subFields fields, query $subQueryLines lines")
+ }
+ "Object" {
+ $subObjName = $subDs.SelectSingleNode("s:objectName", $ns)
+ $subObjStr = if ($subObjName) { " objectName=$($subObjName.InnerText)" } else { "" }
+ $lines.Add(" " + [string][char]0x251C + [string][char]0x2500 + " [Object] $subNameStr$subObjStr $subFields fields")
+ }
+ default {
+ $lines.Add(" " + [string][char]0x251C + [string][char]0x2500 + " [$subType] $subNameStr $subFields fields")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ # Links — only dataset pairs (not field-level)
+ $links = $root.SelectNodes("s:dataSetLink", $ns)
+ if ($links.Count -gt 0) {
+ $linkPairs = [ordered]@{}
+ foreach ($lnk in $links) {
+ $srcDs = $lnk.SelectSingleNode("s:sourceDataSet", $ns).InnerText
+ $dstDs = $lnk.SelectSingleNode("s:destinationDataSet", $ns).InnerText
+ $key = "$srcDs -> $dstDs"
+ if (-not $linkPairs.Contains($key)) { $linkPairs[$key] = 0 }
+ $linkPairs[$key] = $linkPairs[$key] + 1
+ }
+ $linkStrs = @()
+ foreach ($key in $linkPairs.Keys) {
+ $cnt = $linkPairs[$key]
+ if ($cnt -gt 1) { $linkStrs += "$key (${cnt} fields)" }
+ else { $linkStrs += $key }
+ }
+ $lines.Add("Links: " + ($linkStrs -join ", "))
+ }
+
+ # Calculated fields — count only
+ $calcFields = $root.SelectNodes("s:calculatedField", $ns)
+ if ($calcFields.Count -gt 0) {
+ $lines.Add("Calculated: $($calcFields.Count)")
+ }
+
+ # Totals — count + group flag
+ $totalFields = $root.SelectNodes("s:totalField", $ns)
+ if ($totalFields.Count -gt 0) {
+ $hasGrouped = $false
+ $uniquePaths = @{}
+ foreach ($tf in $totalFields) {
+ $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText
+ $uniquePaths[$tfPath] = $true
+ if ($tf.SelectSingleNode("s:group", $ns)) { $hasGrouped = $true }
+ }
+ $groupNote = if ($hasGrouped) { ", with group formulas" } else { "" }
+ if ($uniquePaths.Count -eq $totalFields.Count) {
+ $lines.Add("Resources: $($totalFields.Count)$groupNote")
+ } else {
+ $lines.Add("Resources: $($totalFields.Count) ($($uniquePaths.Count) fields$groupNote)")
+ }
+ }
+
+ # Templates — count with binding types
+ $tplDefs = $root.SelectNodes("s:template", $ns)
+ $fieldTpls = $root.SelectNodes("s:fieldTemplate", $ns)
+ $groupTpls = $root.SelectNodes("s:groupTemplate", $ns)
+ $groupHeaderTpls = $root.SelectNodes("s:groupHeaderTemplate", $ns)
+ $groupFooterTpls = $root.SelectNodes("s:groupFooterTemplate", $ns)
+ $totalBindings = $fieldTpls.Count + $groupTpls.Count + $groupHeaderTpls.Count + $groupFooterTpls.Count
+ if ($tplDefs.Count -gt 0) {
+ $parts = @()
+ if ($fieldTpls.Count -gt 0) { $parts += "$($fieldTpls.Count) field" }
+ $grpCount = $groupTpls.Count + $groupHeaderTpls.Count + $groupFooterTpls.Count
+ if ($grpCount -gt 0) { $parts += "$grpCount group" }
+ $lines.Add("Templates: $($tplDefs.Count) defined ($($parts -join ', ') bindings)")
+ }
+
+ # Parameters — split visible/hidden
+ $params = $root.SelectNodes("s:parameter", $ns)
+ if ($params.Count -gt 0) {
+ $visibleNames = @()
+ $hiddenCount = 0
+ foreach ($p in $params) {
+ $pName = $p.SelectSingleNode("s:name", $ns).InnerText
+ $useRestrict = $p.SelectSingleNode("s:useRestriction", $ns)
+ $isHidden = ($useRestrict -and $useRestrict.InnerText -eq "true")
+ if ($isHidden) { $hiddenCount++ } else { $visibleNames += $pName }
+ }
+ $paramLine = "Params: $($params.Count)"
+ if ($hiddenCount -gt 0 -and $visibleNames.Count -gt 0) {
+ $paramLine += " ($($visibleNames.Count) visible, $hiddenCount hidden)"
+ } elseif ($hiddenCount -eq $params.Count) {
+ $paramLine += " (all hidden)"
+ }
+ if ($visibleNames.Count -gt 0 -and $visibleNames.Count -le 8) {
+ $paramLine += ": " + ($visibleNames -join ", ")
+ }
+ $lines.Add($paramLine)
+ } else {
+ $lines.Add("Params: (none)")
+ }
+
+ $lines.Add("")
+
+ # Variants
+ $variants = $root.SelectNodes("s:settingsVariant", $ns)
+ if ($variants.Count -gt 0) {
+ $lines.Add("Variants:")
+ $varIdx = 0
+ foreach ($v in $variants) {
+ $varIdx++
+ $vName = $v.SelectSingleNode("dcsset:name", $ns).InnerText
+ $vPres = $v.SelectSingleNode("dcsset:presentation", $ns)
+ $vPresStr = ""
+ if ($vPres) {
+ $pt = Get-MLText $vPres
+ if ($pt) { $vPresStr = " `"$pt`"" }
+ }
+
+ $settings = $v.SelectSingleNode("dcsset:settings", $ns)
+ $structItems = @()
+ if ($settings) {
+ foreach ($si in $settings.SelectNodes("dcsset:item", $ns)) {
+ $siType = Get-StructureItemType $si
+ $groupFields = Get-GroupFields $si
+ $groupStr = if ($groupFields.Count -gt 0) { "(" + ($groupFields -join ",") + ")" } else { "(detail)" }
+ $structItems += "$siType$groupStr"
+ }
+ }
+ # Compact: if many identical items, show count
+ if ($structItems.Count -gt 3) {
+ $grouped = $structItems | Group-Object | Sort-Object Count -Descending
+ $compactParts = @()
+ foreach ($g in $grouped) {
+ if ($g.Count -gt 1) { $compactParts += "$($g.Count)x $($g.Name)" }
+ else { $compactParts += $g.Name }
+ }
+ $structItems = $compactParts
+ }
+ $structStr = if ($structItems.Count -gt 0) { " " + ($structItems -join ", ") } else { "" }
+
+ $filterCount = 0
+ if ($settings) {
+ $filterCount = $settings.SelectNodes("dcsset:filter/dcsset:item", $ns).Count
+ }
+ $filterStr = if ($filterCount -gt 0) { " $filterCount filters" } else { "" }
+
+ $lines.Add(" [$varIdx] $vName$vPresStr$structStr$filterStr")
+ }
+ }
+
+ # Hints — suggest next commands
+ $lines.Add("")
+ $hints = @()
+ # Collect query dataset names for hint
+ $queryDsNames = @()
+ foreach ($ds in $root.SelectNodes("s:dataSet", $ns)) {
+ $dsType = Get-DataSetType $ds
+ if ($dsType -eq "Query") {
+ $queryDsNames += $ds.SelectSingleNode("s:name", $ns).InnerText
+ } elseif ($dsType -eq "Union") {
+ foreach ($subDs in $ds.SelectNodes("s:item", $ns)) {
+ if ((Get-DataSetType $subDs) -eq "Query") {
+ $sn = $subDs.SelectSingleNode("s:name", $ns)
+ if ($sn) { $queryDsNames += $sn.InnerText }
+ }
+ }
+ }
+ }
+ if ($queryDsNames.Count -eq 1) {
+ $hints += "-Mode query query text"
+ } elseif ($queryDsNames.Count -gt 1) {
+ $hints += "-Mode query -Name query text ($($queryDsNames -join ', '))"
+ }
+ $hints += "-Mode fields field tables by dataset"
+ $linkCount = $root.SelectNodes("s:dataSetLink", $ns).Count
+ if ($linkCount -gt 0) {
+ $hints += "-Mode links dataset connections ($linkCount)"
+ }
+ $calcCount = $root.SelectNodes("s:calculatedField", $ns).Count
+ $totalCount = $root.SelectNodes("s:totalField", $ns).Count
+ if ($calcCount -gt 0) {
+ $hints += "-Mode calculated calculated field expressions ($calcCount)"
+ }
+ if ($totalCount -gt 0) {
+ $hints += "-Mode resources resource aggregation ($totalCount)"
+ }
+ if ($params.Count -gt 0) {
+ $hints += "-Mode params parameter details"
+ }
+ if ($variants.Count -eq 1) {
+ $hints += "-Mode variant variant structure"
+ } elseif ($variants.Count -gt 1) {
+ $hints += "-Mode variant -Name variant structure (1..$($variants.Count))"
+ }
+ if ($tplDefs.Count -gt 0) {
+ $hints += "-Mode templates template bindings and expressions"
+ }
+ $hints += "-Mode trace -Name trace field origin (by name or title)"
+ $lines.Add("Next:")
+ foreach ($h in $hints) { $lines.Add(" $h") }
+}
+
+# ============================================================
+# MODE: query
+# ============================================================
+elseif ($Mode -eq "query") {
+
+ # Find dataset
+ $dataSets = $root.SelectNodes("s:dataSet", $ns)
+ $targetDs = $null
+
+ if ($Name) {
+ # Search by name: prefer nested Query items over parent Union
+ # Pass 1: search nested items first
+ foreach ($ds in $dataSets) {
+ foreach ($subDs in $ds.SelectNodes("s:item", $ns)) {
+ $subNameNode = $subDs.SelectSingleNode("s:name", $ns)
+ if ($subNameNode -and $subNameNode.InnerText -eq $Name) { $targetDs = $subDs; break }
+ }
+ if ($targetDs) { break }
+ }
+ # Pass 2: search top-level
+ if (-not $targetDs) {
+ foreach ($ds in $dataSets) {
+ $dsNameNode = $ds.SelectSingleNode("s:name", $ns)
+ if ($dsNameNode -and $dsNameNode.InnerText -eq $Name) { $targetDs = $ds; break }
+ }
+ }
+ if (-not $targetDs) {
+ Write-Error "Dataset '$Name' not found"
+ exit 1
+ }
+ } else {
+ # Take first Query dataset
+ foreach ($ds in $dataSets) {
+ $dsType = Get-DataSetType $ds
+ if ($dsType -eq "Query") { $targetDs = $ds; break }
+ if ($dsType -eq "Union") {
+ foreach ($subDs in $ds.SelectNodes("s:item", $ns)) {
+ if ((Get-DataSetType $subDs) -eq "Query") { $targetDs = $subDs; break }
+ }
+ if ($targetDs) { break }
+ }
+ }
+ if (-not $targetDs) {
+ Write-Error "No Query dataset found"
+ exit 1
+ }
+ }
+
+ $queryNode = $targetDs.SelectSingleNode("s:query", $ns)
+ if (-not $queryNode) {
+ # If this is a Union, list nested query datasets
+ $dsType = Get-DataSetType $targetDs
+ if ($dsType -eq "Union") {
+ $subNames = @()
+ foreach ($subDs in $targetDs.SelectNodes("s:item", $ns)) {
+ $sn = $subDs.SelectSingleNode("s:name", $ns)
+ if ($sn) { $subNames += $sn.InnerText }
+ }
+ Write-Error "Dataset '$($targetDs.SelectSingleNode("s:name", $ns).InnerText)' is a Union. Specify nested: $($subNames -join ', ')"
+ } else {
+ Write-Error "Dataset has no query element"
+ }
+ exit 1
+ }
+
+ $rawQuery = Unescape-Xml $queryNode.InnerText
+ $dsNameStr = $targetDs.SelectSingleNode("s:name", $ns).InnerText
+
+ # Split into batches
+ $batches = @()
+ $batchTexts = $rawQuery -split ';\s*\r?\n\s*/{16,}\s*\r?\n'
+ foreach ($bt in $batchTexts) {
+ $trimmed = $bt.Trim()
+ if ($trimmed) { $batches += $trimmed }
+ }
+
+ $totalQueryLines = ($rawQuery -split "`n").Count
+
+ if ($batches.Count -le 1) {
+ # Single query
+ $lines.Add("=== Query: $dsNameStr ($totalQueryLines lines) ===")
+ $lines.Add("")
+ foreach ($ql in ($rawQuery.Trim() -split "`n")) {
+ $lines.Add($ql.TrimEnd())
+ }
+ } else {
+ $lines.Add("=== Query: $dsNameStr ($totalQueryLines lines, $($batches.Count) batches) ===")
+
+ if ($Batch -eq 0) {
+ # Show TOC
+ $lineNum = 1
+ for ($bi = 0; $bi -lt $batches.Count; $bi++) {
+ $batchLines = ($batches[$bi] -split "`n")
+ $endLine = $lineNum + $batchLines.Count - 1
+ # Detect ПОМЕСТИТЬ target
+ $target = ""
+ foreach ($bl in $batchLines) {
+ if ($bl -match '^\s*(?:ПОМЕСТИТЬ|INTO)\s+(\S+)') {
+ $target = [char]0x2192 + " " + $Matches[1]
+ break
+ }
+ }
+ $lines.Add(" Batch $($bi + 1): lines $lineNum-$endLine $target")
+ $lineNum = $endLine + 3 # +separator
+ }
+ $lines.Add("")
+
+ # Show all batches
+ for ($bi = 0; $bi -lt $batches.Count; $bi++) {
+ $lines.Add("--- Batch $($bi + 1) ---")
+ foreach ($ql in ($batches[$bi] -split "`n")) {
+ $lines.Add($ql.TrimEnd())
+ }
+ $lines.Add("")
+ }
+ } else {
+ # Show specific batch
+ if ($Batch -gt $batches.Count) {
+ Write-Error "Batch $Batch not found (total: $($batches.Count))"
+ exit 1
+ }
+ $lines.Add("")
+ $lines.Add("--- Batch $Batch ---")
+ foreach ($ql in ($batches[$Batch - 1] -split "`n")) {
+ $lines.Add($ql.TrimEnd())
+ }
+ }
+ }
+}
+
+# ============================================================
+# MODE: fields
+# ============================================================
+elseif ($Mode -eq "fields") {
+
+ $dataSets = $root.SelectNodes("s:dataSet", $ns)
+
+ function Show-DataSetFields($dsNode) {
+ $dsType = Get-DataSetType $dsNode
+ $dsNameStr = $dsNode.SelectSingleNode("s:name", $ns).InnerText
+ $fields = $dsNode.SelectNodes("s:field", $ns)
+
+ $lines.Add("=== Fields: $dsNameStr [$dsType] ($($fields.Count)) ===")
+ $lines.Add(" dataPath title role restrict format")
+
+ foreach ($f in $fields) {
+ $dp = $f.SelectSingleNode("s:dataPath", $ns)
+ $dpStr = if ($dp) { $dp.InnerText } else { "-" }
+
+ $titleNode = $f.SelectSingleNode("s:title", $ns)
+ $titleStr = if ($titleNode) { Get-MLText $titleNode } else { "" }
+ if (-not $titleStr) { $titleStr = "-" }
+
+ # Role
+ $role = $f.SelectSingleNode("s:role", $ns)
+ $roleStr = "-"
+ if ($role) {
+ $roleParts = @()
+ foreach ($child in $role.ChildNodes) {
+ if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") {
+ $roleParts += $child.LocalName
+ }
+ }
+ if ($roleParts.Count -gt 0) { $roleStr = $roleParts -join "," }
+ }
+
+ # UseRestriction
+ $restrict = $f.SelectSingleNode("s:useRestriction", $ns)
+ $restrictStr = "-"
+ if ($restrict) {
+ $restrictParts = @()
+ foreach ($child in $restrict.ChildNodes) {
+ if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") {
+ $restrictParts += $child.LocalName.Substring(0, [Math]::Min(4, $child.LocalName.Length))
+ }
+ }
+ if ($restrictParts.Count -gt 0) { $restrictStr = $restrictParts -join "," }
+ }
+
+ # Appearance format
+ $formatStr = "-"
+ $appearance = $f.SelectSingleNode("s:appearance", $ns)
+ if ($appearance) {
+ foreach ($appItem in $appearance.SelectNodes("dcscor:item", $ns)) {
+ $paramNode = $appItem.SelectSingleNode("dcscor:parameter", $ns)
+ $valNode = $appItem.SelectSingleNode("dcscor:value", $ns)
+ if ($paramNode -and ($paramNode.InnerText -eq "Формат" -or $paramNode.InnerText -eq "Format") -and $valNode) {
+ $formatStr = $valNode.InnerText
+ }
+ }
+ }
+
+ # presentationExpression
+ $presExpr = $f.SelectSingleNode("s:presentationExpression", $ns)
+ $presStr = ""
+ if ($presExpr) { $presStr = " presExpr" }
+
+ $dpPad = $dpStr.PadRight(35)
+ $titlePad = $titleStr.PadRight(22)
+ $rolePad = $roleStr.PadRight(10)
+ $restrictPad = $restrictStr.PadRight(12)
+
+ $lines.Add(" $dpPad $titlePad $rolePad $restrictPad $formatStr$presStr")
+ }
+ }
+
+ if ($Name) {
+ # Detail for specific field by dataPath — search all datasets
+ $found = $false
+ $matchedIn = @()
+
+ function Collect-FieldInfo($dsNode) {
+ $dsType = Get-DataSetType $dsNode
+ $dsNameStr = $dsNode.SelectSingleNode("s:name", $ns).InnerText
+ foreach ($f in $dsNode.SelectNodes("s:field", $ns)) {
+ $dp = $f.SelectSingleNode("s:dataPath", $ns)
+ if (-not $dp -or $dp.InnerText -ne $Name) { continue }
+
+ $info = @{ dataset = "$dsNameStr [$dsType]" }
+
+ $titleNode = $f.SelectSingleNode("s:title", $ns)
+ $info.title = if ($titleNode) { Get-MLText $titleNode } else { "" }
+
+ # ValueType
+ $vt = $f.SelectSingleNode("s:valueType", $ns)
+ $info.type = if ($vt) { Get-CompactType $vt } else { "" }
+
+ # Role
+ $role = $f.SelectSingleNode("s:role", $ns)
+ $roleParts = @()
+ if ($role) {
+ foreach ($child in $role.ChildNodes) {
+ if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") {
+ $roleParts += $child.LocalName
+ }
+ }
+ }
+ $info.role = $roleParts -join ", "
+
+ # UseRestriction
+ $restrict = $f.SelectSingleNode("s:useRestriction", $ns)
+ $restrictParts = @()
+ if ($restrict) {
+ foreach ($child in $restrict.ChildNodes) {
+ if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") {
+ $restrictParts += $child.LocalName
+ }
+ }
+ }
+ $info.restrict = $restrictParts -join ", "
+
+ # Format
+ $formatStr = ""
+ $appearance = $f.SelectSingleNode("s:appearance", $ns)
+ if ($appearance) {
+ foreach ($appItem in $appearance.SelectNodes("dcscor:item", $ns)) {
+ $pn = $appItem.SelectSingleNode("dcscor:parameter", $ns)
+ $vn = $appItem.SelectSingleNode("dcscor:value", $ns)
+ if ($pn -and ($pn.InnerText -eq "Формат" -or $pn.InnerText -eq "Format") -and $vn) {
+ $formatStr = $vn.InnerText
+ }
+ }
+ }
+ $info.format = $formatStr
+
+ # PresentationExpression
+ $presExpr = $f.SelectSingleNode("s:presentationExpression", $ns)
+ $info.presExpr = if ($presExpr) { $presExpr.InnerText } else { "" }
+
+ return $info
+ }
+ return $null
+ }
+
+ # Search all datasets and nested items
+ $fieldInfos = @()
+ foreach ($ds in $dataSets) {
+ $info = Collect-FieldInfo $ds
+ if ($info) { $fieldInfos += $info }
+ $dsType = Get-DataSetType $ds
+ if ($dsType -eq "Union") {
+ foreach ($subDs in $ds.SelectNodes("s:item", $ns)) {
+ $info = Collect-FieldInfo $subDs
+ if ($info) { $fieldInfos += $info }
+ }
+ }
+ }
+
+ if ($fieldInfos.Count -eq 0) {
+ Write-Error "Field '$Name' not found in any dataset"
+ exit 1
+ }
+
+ # Use first match for detail (they usually share the same properties)
+ $first = $fieldInfos[0]
+ $titleStr = if ($first.title) { " `"$($first.title)`"" } else { "" }
+ $lines.Add("=== Field: $Name$titleStr ===")
+ $lines.Add("")
+
+ # Datasets
+ $dsList = ($fieldInfos | ForEach-Object { $_.dataset }) -join ", "
+ $lines.Add("Dataset: $dsList")
+
+ if ($first.type) { $lines.Add("Type: $($first.type)") }
+ if ($first.role) { $lines.Add("Role: $($first.role)") }
+ if ($first.restrict) { $lines.Add("Restrict: $($first.restrict)") }
+ if ($first.format) { $lines.Add("Format: $($first.format)") }
+ if ($first.presExpr) {
+ $lines.Add("PresentationExpression:")
+ foreach ($el in ($first.presExpr -split "`n")) { $lines.Add(" $($el.TrimEnd())") }
+ }
+ } else {
+ # Compact map: field names per dataset
+ $lines.Add("=== Fields map ===")
+
+ function Show-DataSetFieldMap($dsNode, $indent) {
+ $dsType = Get-DataSetType $dsNode
+ $dsNameStr = $dsNode.SelectSingleNode("s:name", $ns).InnerText
+ $fields = $dsNode.SelectNodes("s:field", $ns)
+ $fieldNames = @()
+ foreach ($f in $fields) {
+ $dp = $f.SelectSingleNode("s:dataPath", $ns)
+ if ($dp) { $fieldNames += $dp.InnerText }
+ }
+ $nameList = $fieldNames -join ", "
+ if ($nameList.Length -gt 100) {
+ $nameList = $nameList.Substring(0, 97) + "..."
+ }
+ $lines.Add("$indent$dsNameStr [$dsType] ($($fields.Count)): $nameList")
+ }
+
+ foreach ($ds in $dataSets) {
+ Show-DataSetFieldMap $ds ""
+ $dsType = Get-DataSetType $ds
+ if ($dsType -eq "Union") {
+ foreach ($subDs in $ds.SelectNodes("s:item", $ns)) {
+ Show-DataSetFieldMap $subDs " "
+ }
+ }
+ }
+
+ $lines.Add("")
+ $lines.Add("Use -Name for details.")
+ }
+}
+
+# ============================================================
+# MODE: links
+# ============================================================
+elseif ($Mode -eq "links") {
+
+ $links = $root.SelectNodes("s:dataSetLink", $ns)
+ if ($links.Count -eq 0) {
+ $lines.Add("(no links)")
+ } else {
+ $lines.Add("=== Links ($($links.Count)) ===")
+ $lines.Add("")
+ # Group by source->dest pair
+ $currentPair = ""
+ foreach ($lnk in $links) {
+ $srcDs = $lnk.SelectSingleNode("s:sourceDataSet", $ns).InnerText
+ $dstDs = $lnk.SelectSingleNode("s:destinationDataSet", $ns).InnerText
+ $srcExpr = $lnk.SelectSingleNode("s:sourceExpression", $ns).InnerText
+ $dstExpr = $lnk.SelectSingleNode("s:destinationExpression", $ns).InnerText
+ $paramNode = $lnk.SelectSingleNode("s:parameter", $ns)
+ $paramListNode = $lnk.SelectSingleNode("s:parameterListAllowed", $ns)
+
+ $pair = "$srcDs -> $dstDs"
+ if ($pair -ne $currentPair) {
+ if ($currentPair) { $lines.Add("") }
+ $lines.Add("$pair :")
+ $currentPair = $pair
+ }
+
+ $paramStr = ""
+ if ($paramNode) { $paramStr = " param=$($paramNode.InnerText)" }
+
+ $lines.Add(" $srcExpr -> $dstExpr$paramStr")
+ }
+ }
+}
+
+# ============================================================
+# MODE: calculated
+# ============================================================
+elseif ($Mode -eq "calculated") {
+
+ $calcFields = $root.SelectNodes("s:calculatedField", $ns)
+ if ($calcFields.Count -eq 0) {
+ $lines.Add("(no calculated fields)")
+ } elseif ($Name) {
+ $found = $false
+ foreach ($cf in $calcFields) {
+ $cfPath = $cf.SelectSingleNode("s:dataPath", $ns).InnerText
+ if ($cfPath -eq $Name) {
+ $lines.Add("=== Calculated: $cfPath ===")
+ $lines.Add("")
+
+ $cfExpr = $cf.SelectSingleNode("s:expression", $ns).InnerText
+ $lines.Add("Expression:")
+ foreach ($el in ($cfExpr -split "`n")) { $lines.Add(" $($el.TrimEnd())") }
+
+ $cfTitle = $cf.SelectSingleNode("s:title", $ns)
+ if ($cfTitle) {
+ $t = Get-MLText $cfTitle
+ if ($t) { $lines.Add("Title: $t") }
+ }
+
+ $cfRestrict = $cf.SelectSingleNode("s:useRestriction", $ns)
+ if ($cfRestrict) {
+ $parts = @()
+ foreach ($child in $cfRestrict.ChildNodes) {
+ if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") {
+ $parts += $child.LocalName
+ }
+ }
+ if ($parts.Count -gt 0) { $lines.Add("Restrict: $($parts -join ', ')") }
+ }
+
+ $found = $true
+ break
+ }
+ }
+ if (-not $found) {
+ Write-Error "Calculated field '$Name' not found"
+ exit 1
+ }
+ } else {
+ # Map
+ $lines.Add("=== Calculated fields ($($calcFields.Count)) ===")
+ foreach ($cf in $calcFields) {
+ $cfPath = $cf.SelectSingleNode("s:dataPath", $ns).InnerText
+ $cfTitle = $cf.SelectSingleNode("s:title", $ns)
+ $titleStr = ""
+ if ($cfTitle) {
+ $t = Get-MLText $cfTitle
+ if ($t -and $t -ne $cfPath) { $titleStr = " `"$t`"" }
+ }
+ $lines.Add(" $cfPath$titleStr")
+ }
+ $lines.Add("")
+ $lines.Add("Use -Name for full expression.")
+ }
+}
+
+# ============================================================
+# MODE: resources
+# ============================================================
+elseif ($Mode -eq "resources") {
+
+ $totalFields = $root.SelectNodes("s:totalField", $ns)
+ if ($totalFields.Count -eq 0) {
+ $lines.Add("(no resources)")
+ } elseif ($Name) {
+ $matched = @()
+ foreach ($tf in $totalFields) {
+ $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText
+ if ($tfPath -eq $Name) { $matched += $tf }
+ }
+ if ($matched.Count -eq 0) {
+ Write-Error "Resource '$Name' not found"
+ exit 1
+ }
+ $lines.Add("=== Resource: $Name ===")
+ $lines.Add("")
+ foreach ($tf in $matched) {
+ $tfExpr = $tf.SelectSingleNode("s:expression", $ns).InnerText
+ $tfGroup = $tf.SelectSingleNode("s:group", $ns)
+ $groupStr = "(overall)"
+ if ($tfGroup) { $groupStr = $tfGroup.InnerText }
+ $lines.Add(" [$groupStr] $tfExpr")
+ }
+ } else {
+ # Map
+ $lines.Add("=== Resources ($($totalFields.Count)) ===")
+ $resMap = [ordered]@{}
+ foreach ($tf in $totalFields) {
+ $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText
+ $tfGroup = $tf.SelectSingleNode("s:group", $ns)
+ if (-not $resMap.Contains($tfPath)) {
+ $resMap[$tfPath] = @{ hasGroup = $false }
+ }
+ if ($tfGroup) { $resMap[$tfPath].hasGroup = $true }
+ }
+ foreach ($key in $resMap.Keys) {
+ $groupMark = if ($resMap[$key].hasGroup) { " *" } else { "" }
+ $lines.Add(" $key$groupMark")
+ }
+ $lines.Add("")
+ $lines.Add(" * = has group-level formulas")
+ $lines.Add("")
+ $lines.Add("Use -Name for full formula.")
+ }
+}
+
+# ============================================================
+# MODE: params
+# ============================================================
+elseif ($Mode -eq "params") {
+
+ $params = $root.SelectNodes("s:parameter", $ns)
+ $lines.Add("=== Parameters ($($params.Count)) ===")
+ $lines.Add(" Name Type Default Visible Expression")
+
+ foreach ($p in $params) {
+ $pName = $p.SelectSingleNode("s:name", $ns).InnerText
+ $pType = Get-CompactType $p.SelectSingleNode("s:valueType", $ns)
+ if (-not $pType) { $pType = "-" }
+
+ # Default value
+ $valNode = $p.SelectSingleNode("s:value", $ns)
+ $valStr = "-"
+ if ($valNode) {
+ $nilAttr = $valNode.GetAttribute("nil", "http://www.w3.org/2001/XMLSchema-instance")
+ if ($nilAttr -eq "true") {
+ $valStr = "null"
+ } else {
+ $raw = $valNode.InnerText.Trim()
+ if ($raw -eq "0001-01-01T00:00:00") {
+ $valStr = "-"
+ } elseif ($raw) {
+ # Check for StandardPeriod variant
+ $variant = $valNode.SelectSingleNode("v8:variant", $ns)
+ if ($variant) {
+ $valStr = $variant.InnerText
+ } else {
+ $valStr = $raw
+ if ($valStr.Length -gt 15) { $valStr = $valStr.Substring(0, 12) + "..." }
+ }
+ }
+ }
+ }
+
+ # Visibility
+ $useRestrict = $p.SelectSingleNode("s:useRestriction", $ns)
+ $visStr = "yes"
+ if ($useRestrict -and $useRestrict.InnerText -eq "true") { $visStr = "hidden" }
+ if ($useRestrict -and $useRestrict.InnerText -eq "false") { $visStr = "yes" }
+
+ # Expression
+ $exprNode = $p.SelectSingleNode("s:expression", $ns)
+ $exprStr = "-"
+ if ($exprNode -and $exprNode.InnerText.Trim()) {
+ $exprStr = Unescape-Xml $exprNode.InnerText.Trim()
+ }
+
+ # availableAsField
+ $availField = $p.SelectSingleNode("s:availableAsField", $ns)
+ $availStr = ""
+ if ($availField -and $availField.InnerText -eq "false") { $availStr = " [noField]" }
+
+ $namePad = $pName.PadRight(33)
+ $typePad = $pType.PadRight(22)
+ $valPad = $valStr.PadRight(16)
+ $visPad = $visStr.PadRight(8)
+
+ $lines.Add(" $namePad $typePad $valPad $visPad $exprStr$availStr")
+ }
+}
+
+# ============================================================
+# MODE: variant
+# ============================================================
+elseif ($Mode -eq "variant") {
+
+ $variants = $root.SelectNodes("s:settingsVariant", $ns)
+
+ if (-not $Name) {
+ # --- Variant list (map) ---
+ if ($variants.Count -eq 0) {
+ $lines.Add("=== Variants: (none) ===")
+ } else {
+ $lines.Add("=== Variants ($($variants.Count)) ===")
+ $varIdx = 0
+ foreach ($v in $variants) {
+ $varIdx++
+ $vName = $v.SelectSingleNode("dcsset:name", $ns).InnerText
+ $vPres = $v.SelectSingleNode("dcsset:presentation", $ns)
+ $vPresStr = ""
+ if ($vPres) {
+ $pt = Get-MLText $vPres
+ if ($pt) { $vPresStr = " `"$pt`"" }
+ }
+
+ $settings = $v.SelectSingleNode("dcsset:settings", $ns)
+ $structItems = @()
+ if ($settings) {
+ foreach ($si in $settings.SelectNodes("dcsset:item", $ns)) {
+ $siType = Get-StructureItemType $si
+ $groupFields = Get-GroupFields $si
+ $groupStr = if ($groupFields.Count -gt 0) { "(" + ($groupFields -join ",") + ")" } else { "(detail)" }
+ $structItems += "$siType$groupStr"
+ }
+ }
+ if ($structItems.Count -gt 3) {
+ $grouped = $structItems | Group-Object | Sort-Object Count -Descending
+ $compactParts = @()
+ foreach ($g in $grouped) {
+ if ($g.Count -gt 1) { $compactParts += "$($g.Count)x $($g.Name)" }
+ else { $compactParts += $g.Name }
+ }
+ $structItems = $compactParts
+ }
+ $structStr = if ($structItems.Count -gt 0) { " " + ($structItems -join ", ") } else { "" }
+
+ $filterCount = 0
+ if ($settings) {
+ $filterCount = $settings.SelectNodes("dcsset:filter/dcsset:item", $ns).Count
+ }
+ $filterStr = if ($filterCount -gt 0) { " $filterCount filters" } else { "" }
+
+ # Selection fields
+ $selFields = @()
+ if ($settings) { $selFields = Get-SelectionFields $settings }
+ $selStr = if ($selFields.Count -gt 0) { " sel: " + ($selFields -join ", ") } else { "" }
+
+ $lines.Add(" [$varIdx] $vName$vPresStr$structStr$filterStr")
+ if ($selStr) { $lines.Add(" $selStr") }
+ }
+ }
+ } else {
+ # --- Variant detail ---
+
+ $targetVariant = $null
+ $varIdx = 0
+ foreach ($v in $variants) {
+ $varIdx++
+ $vName = $v.SelectSingleNode("dcsset:name", $ns).InnerText
+ if ($vName -eq $Name -or "$varIdx" -eq $Name) {
+ $targetVariant = $v
+ $matchIdx = $varIdx
+ break
+ }
+ }
+ if (-not $targetVariant) {
+ Write-Error "Variant '$Name' not found. Use -Mode variant without -Name to see list."
+ exit 1
+ }
+
+ $vName = $targetVariant.SelectSingleNode("dcsset:name", $ns).InnerText
+ $vPres = $targetVariant.SelectSingleNode("dcsset:presentation", $ns)
+ $vPresStr = ""
+ if ($vPres) {
+ $pt = Get-MLText $vPres
+ if ($pt) { $vPresStr = " `"$pt`"" }
+ }
+
+ $lines.Add("=== Variant [$matchIdx]: $vName$vPresStr ===")
+
+ $settings = $targetVariant.SelectSingleNode("dcsset:settings", $ns)
+ if (-not $settings) {
+ $lines.Add(" (empty settings)")
+ } else {
+ # Selection at settings level
+ $topSel = Get-SelectionFields $settings
+ if ($topSel.Count -gt 0) {
+ $lines.Add("")
+ $lines.Add("Selection: " + ($topSel -join ", "))
+ }
+
+ # Structure
+ $structItems = $settings.SelectNodes("dcsset:item", $ns)
+ $hasStruct = $false
+ foreach ($si in $structItems) {
+ $siXsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
+ if ($siXsiType -like "*StructureItem*") { $hasStruct = $true; break }
+ }
+
+ if ($hasStruct) {
+ $lines.Add("")
+ $lines.Add("Structure:")
+ foreach ($si in $structItems) {
+ $siXsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
+ if ($siXsiType -like "*StructureItem*") {
+ Build-StructureTree -itemNode $si -prefix " " -isLast $false -outLines $lines
+ }
+ }
+ }
+
+ # Filter
+ $filters = Get-FilterSummary $settings
+ if ($filters.Count -gt 0) {
+ $lines.Add("")
+ $lines.Add("Filter:")
+ foreach ($f in $filters) {
+ $lines.Add(" $f")
+ }
+ }
+
+ # Data parameters
+ $dataParams = $settings.SelectNodes("dcsset:dataParameters/dcsset:item", $ns)
+ if ($dataParams.Count -gt 0) {
+ $dpStrs = @()
+ foreach ($dp in $dataParams) {
+ $dpParam = $dp.SelectSingleNode("dcscor:parameter", $ns)
+ $dpVal = $dp.SelectSingleNode("dcscor:value", $ns)
+ if ($dpParam -and $dpVal) {
+ $dpStrs += "$($dpParam.InnerText)=`"$($dpVal.InnerText)`""
+ }
+ }
+ if ($dpStrs.Count -gt 0) {
+ $lines.Add("")
+ $lines.Add("DataParams: " + ($dpStrs -join ", "))
+ }
+ }
+
+ # Output parameters
+ $outParams = $settings.SelectNodes("dcsset:outputParameters/dcscor:item", $ns)
+ if ($outParams.Count -gt 0) {
+ $opStrs = @()
+ foreach ($op in $outParams) {
+ $opParam = $op.SelectSingleNode("dcscor:parameter", $ns)
+ $opVal = $op.SelectSingleNode("dcscor:value", $ns)
+ if ($opParam -and $opVal) {
+ $paramName = $opParam.InnerText
+ $paramVal = $opVal.InnerText
+ # Shorten known long names
+ switch ($paramName) {
+ "МакетОформления" { $opStrs += "style=$paramVal" }
+ "РасположениеПолейГруппировки" { $opStrs += "groups=$paramVal" }
+ "ГоризонтальноеРасположениеОбщихИтогов" { $opStrs += "totalsH=$paramVal" }
+ "ВертикальноеРасположениеОбщихИтогов" { $opStrs += "totalsV=$paramVal" }
+ "ВыводитьЗаголовок" { $opStrs += "header=$paramVal" }
+ "ВыводитьОтбор" { $opStrs += "filter=$paramVal" }
+ "ВыводитьПараметрыДанных" { $opStrs += "dataParams=$paramVal" }
+ "РасположениеРеквизитов" { $opStrs += "attrs=$paramVal" }
+ default { $opStrs += "$paramName=$paramVal" }
+ }
+ }
+ }
+ if ($opStrs.Count -gt 0) {
+ $lines.Add("")
+ $lines.Add("Output: " + ($opStrs -join " "))
+ }
+ }
+ }
+ } # end else (variant detail)
+}
+
+# ============================================================
+# MODE: trace
+# ============================================================
+elseif ($Mode -eq "trace") {
+
+ if (-not $Name) {
+ Write-Error "Trace mode requires -Name "
+ exit 1
+ }
+
+ # --- Build field index ---
+
+ $dsFields = @{} # dataPath -> @{ datasets=@(); title="" }
+ $calcFields = @{} # dataPath -> @{ expression=""; title="" }
+ $resFields = @{} # dataPath -> @(@{ expression=""; group="" })
+ $titleMap = @{} # title -> dataPath
+
+ # Scan dataset fields (including nested Union items)
+ $dataSets = $root.SelectNodes("s:dataSet", $ns)
+ foreach ($ds in $dataSets) {
+ $dsName = $ds.SelectSingleNode("s:name", $ns).InnerText
+ $dsType = Get-DataSetType $ds
+
+ foreach ($f in $ds.SelectNodes("s:field", $ns)) {
+ $dp = $f.SelectSingleNode("s:dataPath", $ns)
+ if (-not $dp) { continue }
+ $dpStr = $dp.InnerText
+ if (-not $dsFields.ContainsKey($dpStr)) {
+ $dsFields[$dpStr] = @{ datasets = @(); title = "" }
+ }
+ $dsFields[$dpStr].datasets += "$dsName [$dsType]"
+ $titleNode = $f.SelectSingleNode("s:title", $ns)
+ if ($titleNode) {
+ $t = Get-MLText $titleNode
+ if ($t) {
+ if (-not $dsFields[$dpStr].title) { $dsFields[$dpStr].title = $t }
+ if (-not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr }
+ }
+ }
+ }
+
+ if ($dsType -eq "Union") {
+ foreach ($subDs in $ds.SelectNodes("s:item", $ns)) {
+ $subName = $subDs.SelectSingleNode("s:name", $ns).InnerText
+ $subType = Get-DataSetType $subDs
+ foreach ($f in $subDs.SelectNodes("s:field", $ns)) {
+ $dp = $f.SelectSingleNode("s:dataPath", $ns)
+ if (-not $dp) { continue }
+ $dpStr = $dp.InnerText
+ if (-not $dsFields.ContainsKey($dpStr)) {
+ $dsFields[$dpStr] = @{ datasets = @(); title = "" }
+ }
+ $dsFields[$dpStr].datasets += "$subName [$subType]"
+ $titleNode = $f.SelectSingleNode("s:title", $ns)
+ if ($titleNode) {
+ $t = Get-MLText $titleNode
+ if ($t) {
+ if (-not $dsFields[$dpStr].title) { $dsFields[$dpStr].title = $t }
+ if (-not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ # Scan calculated fields
+ foreach ($cf in $root.SelectNodes("s:calculatedField", $ns)) {
+ $dpStr = $cf.SelectSingleNode("s:dataPath", $ns).InnerText
+ $expr = $cf.SelectSingleNode("s:expression", $ns).InnerText
+ $cfTitle = $cf.SelectSingleNode("s:title", $ns)
+ $t = ""
+ if ($cfTitle) { $t = Get-MLText $cfTitle }
+ $calcFields[$dpStr] = @{ expression = $expr; title = $t }
+ if ($t -and -not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr }
+ }
+
+ # Scan resources
+ foreach ($tf in $root.SelectNodes("s:totalField", $ns)) {
+ $dpStr = $tf.SelectSingleNode("s:dataPath", $ns).InnerText
+ $expr = $tf.SelectSingleNode("s:expression", $ns).InnerText
+ $grp = $tf.SelectSingleNode("s:group", $ns)
+ $groupStr = "(overall)"
+ if ($grp) { $groupStr = $grp.InnerText }
+ if (-not $resFields.ContainsKey($dpStr)) { $resFields[$dpStr] = @() }
+ $resFields[$dpStr] += @{ expression = $expr; group = $groupStr }
+ }
+
+ # --- Resolve name: try dataPath, then exact title, then substring title ---
+ $targetPath = $Name
+ $knownPaths = @()
+ $knownPaths += $dsFields.Keys
+ $knownPaths += $calcFields.Keys
+ $knownPaths += $resFields.Keys
+ $isKnown = $knownPaths -contains $Name
+
+ if (-not $isKnown) {
+ if ($titleMap.ContainsKey($Name)) {
+ $targetPath = $titleMap[$Name]
+ } else {
+ # Substring match in titles
+ $matchedTitle = $null
+ foreach ($key in $titleMap.Keys) {
+ if ($key -like "*$Name*") {
+ $matchedTitle = $key
+ break
+ }
+ }
+ if ($matchedTitle) {
+ $targetPath = $titleMap[$matchedTitle]
+ } else {
+ Write-Error "Field '$Name' not found by dataPath or title"
+ exit 1
+ }
+ }
+ }
+
+ # --- Build output ---
+ $title = ""
+ if ($calcFields.ContainsKey($targetPath) -and $calcFields[$targetPath].title) {
+ $title = $calcFields[$targetPath].title
+ } elseif ($dsFields.ContainsKey($targetPath) -and $dsFields[$targetPath].title) {
+ $title = $dsFields[$targetPath].title
+ }
+ $titleStr = if ($title) { " `"$title`"" } else { "" }
+
+ $lines.Add("=== Trace: $targetPath$titleStr ===")
+ $lines.Add("")
+
+ # Dataset origin
+ if ($dsFields.ContainsKey($targetPath)) {
+ $uniqueDs = $dsFields[$targetPath].datasets | Select-Object -Unique
+ $lines.Add("Dataset: $($uniqueDs -join ', ')")
+ } else {
+ $lines.Add("Dataset: (schema-level only, not in dataset fields)")
+ }
+
+ # Calculated field
+ if ($calcFields.ContainsKey($targetPath)) {
+ $cf = $calcFields[$targetPath]
+ $lines.Add("")
+ $lines.Add("Calculated:")
+ foreach ($el in ($cf.expression -split "`n")) { $lines.Add(" $($el.TrimEnd())") }
+
+ # Extract operands: find known field names in expression
+ $operands = @()
+ $allKnown = @()
+ $allKnown += $dsFields.Keys
+ $allKnown += $calcFields.Keys
+ $allKnown = $allKnown | Select-Object -Unique | Where-Object { $_ -ne $targetPath }
+ # Sort by length descending to match longer names first
+ $allKnown = $allKnown | Sort-Object -Property Length -Descending
+
+ foreach ($fieldName in $allKnown) {
+ $escaped = [regex]::Escape($fieldName)
+ if ($cf.expression -match "(? calculated")
+ } elseif ($dsFields.ContainsKey($op)) {
+ $opDs = ($dsFields[$op].datasets | Select-Object -Unique) -join ", "
+ $lines.Add(" $op -> $opDs")
+ } else {
+ $lines.Add(" $op")
+ }
+ }
+ }
+ }
+
+ # Resource
+ if ($resFields.ContainsKey($targetPath)) {
+ $lines.Add("")
+ $lines.Add("Resource:")
+ foreach ($r in $resFields[$targetPath]) {
+ $lines.Add(" [$($r.group)] $($r.expression)")
+ }
+ }
+
+ # Simple dataset field, no calc/resource
+ if (-not $calcFields.ContainsKey($targetPath) -and -not $resFields.ContainsKey($targetPath)) {
+ if ($dsFields.ContainsKey($targetPath)) {
+ $lines.Add("")
+ $lines.Add("(direct dataset field, no calculated expression or resource)")
+ }
+ }
+}
+
+# ============================================================
+# MODE: templates
+# ============================================================
+elseif ($Mode -eq "templates") {
+
+ # --- Helper: check if expression is trivial ---
+ function Is-TrivialExpr([string]$paramName, [string]$expr) {
+ $e = $expr.Trim()
+ $n = $paramName.Trim()
+ if ($e -eq $n) { return $true }
+ if ($e -eq "Представление($n)") { return $true }
+ return $false
+ }
+
+ # --- Helper: parse template content (rows/cells) ---
+ function Get-TemplateContent([System.Xml.XmlNode]$tplNode) {
+ $innerT = $tplNode.SelectSingleNode("s:template", $ns)
+ if (-not $innerT) { return @{ rows = 0; cells = @(); params = @(); nonTrivial = @() } }
+
+ $rows = $innerT.SelectNodes("dcsat:item", $ns)
+ $rowCount = $rows.Count
+ $cellData = [System.Collections.ArrayList]::new()
+ $rowIdx = 0
+ foreach ($row in $rows) {
+ $rowIdx++
+ $rowCells = [System.Collections.ArrayList]::new()
+ foreach ($cell in $row.SelectNodes("dcsat:tableCell", $ns)) {
+ $field = $cell.SelectSingleNode("dcsat:item", $ns)
+ if (-not $field) {
+ [void]$rowCells.Add("(empty)")
+ continue
+ }
+ $val = $field.SelectSingleNode("dcsat:value", $ns)
+ if (-not $val) {
+ [void]$rowCells.Add("(empty)")
+ continue
+ }
+ $xsiType = $val.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
+ if ($xsiType -like "*LocalStringType*") {
+ $text = Get-MLText $val
+ if ($text) { [void]$rowCells.Add("`"$text`"") }
+ else { [void]$rowCells.Add("(empty)") }
+ } elseif ($xsiType -like "*Parameter*") {
+ [void]$rowCells.Add("{$($val.InnerText)}")
+ } else {
+ [void]$rowCells.Add("(?)")
+ }
+ }
+ [void]$cellData.Add(@{ row = $rowIdx; cells = $rowCells.ToArray() })
+ }
+
+ # Parameters
+ $paramNodes = $tplNode.SelectNodes("s:parameter", $ns)
+ $paramList = [System.Collections.ArrayList]::new()
+ $nonTrivialList = [System.Collections.ArrayList]::new()
+ foreach ($p in $paramNodes) {
+ $pn = $p.SelectSingleNode("dcsat:name", $ns)
+ $pe = $p.SelectSingleNode("dcsat:expression", $ns)
+ if ($pn -and $pe) {
+ $pName = $pn.InnerText
+ $pExpr = $pe.InnerText
+ [void]$paramList.Add(@{ name = $pName; expression = $pExpr })
+ if (-not (Is-TrivialExpr $pName $pExpr)) {
+ [void]$nonTrivialList.Add(@{ name = $pName; expression = $pExpr })
+ }
+ }
+ }
+
+ return @{
+ rows = $rowCount
+ cells = $cellData.ToArray()
+ params = $paramList.ToArray()
+ nonTrivial = $nonTrivialList.ToArray()
+ }
+ }
+
+ # --- Build template name -> node index ---
+ $tplIndex = @{}
+ foreach ($t in $root.SelectNodes("s:template", $ns)) {
+ $tn = $t.SelectSingleNode("s:name", $ns)
+ if ($tn) { $tplIndex[$tn.InnerText] = $t }
+ }
+
+ # --- Parse bindings ---
+ # Group bindings: groupTemplate + groupHeaderTemplate
+ $groupBindings = [ordered]@{} # groupName -> @{ bindings = @(@{type; tplName; tplNode}) }
+
+ foreach ($gt in $root.SelectNodes("s:groupTemplate", $ns)) {
+ $gn = $gt.SelectSingleNode("s:groupName", $ns)
+ $gf = $gt.SelectSingleNode("s:groupField", $ns)
+ $gnStr = if ($gn) { $gn.InnerText } elseif ($gf) { $gf.InnerText } else { "(default)" }
+ $tt = $gt.SelectSingleNode("s:templateType", $ns)
+ $tn = $gt.SelectSingleNode("s:template", $ns)
+ $ttStr = if ($tt) { $tt.InnerText } else { "-" }
+ $tnStr = if ($tn) { $tn.InnerText } else { "-" }
+
+ if (-not $groupBindings.Contains($gnStr)) {
+ $groupBindings[$gnStr] = [System.Collections.ArrayList]::new()
+ }
+ [void]$groupBindings[$gnStr].Add(@{ type = $ttStr; tplName = $tnStr })
+ }
+
+ foreach ($ght in $root.SelectNodes("s:groupHeaderTemplate", $ns)) {
+ $gn = $ght.SelectSingleNode("s:groupName", $ns)
+ $gf = $ght.SelectSingleNode("s:groupField", $ns)
+ $gnStr = if ($gn) { $gn.InnerText } elseif ($gf) { $gf.InnerText } else { "(default)" }
+ $tt = $ght.SelectSingleNode("s:templateType", $ns)
+ $tn = $ght.SelectSingleNode("s:template", $ns)
+ $ttStr = if ($tt) { "GroupHeader" } else { "GroupHeader" }
+ $tnStr = if ($tn) { $tn.InnerText } else { "-" }
+
+ if (-not $groupBindings.Contains($gnStr)) {
+ $groupBindings[$gnStr] = [System.Collections.ArrayList]::new()
+ }
+ [void]$groupBindings[$gnStr].Add(@{ type = $ttStr; tplName = $tnStr })
+ }
+
+ foreach ($gft in $root.SelectNodes("s:groupFooterTemplate", $ns)) {
+ $gn = $gft.SelectSingleNode("s:groupName", $ns)
+ $gf = $gft.SelectSingleNode("s:groupField", $ns)
+ $gnStr = if ($gn) { $gn.InnerText } elseif ($gf) { $gf.InnerText } else { "(default)" }
+ $tn = $gft.SelectSingleNode("s:template", $ns)
+ $tnStr = if ($tn) { $tn.InnerText } else { "-" }
+
+ if (-not $groupBindings.Contains($gnStr)) {
+ $groupBindings[$gnStr] = [System.Collections.ArrayList]::new()
+ }
+ [void]$groupBindings[$gnStr].Add(@{ type = "GroupFooter"; tplName = $tnStr })
+ }
+
+ # Field bindings: fieldTemplate
+ $fieldBindings = [ordered]@{} # fieldName -> tplName
+ $fieldNonTrivial = [System.Collections.ArrayList]::new()
+
+ foreach ($ft in $root.SelectNodes("s:fieldTemplate", $ns)) {
+ $fn = $ft.SelectSingleNode("s:field", $ns)
+ $tn = $ft.SelectSingleNode("s:template", $ns)
+ if ($fn -and $tn) {
+ $fName = $fn.InnerText
+ $tName = $tn.InnerText
+ $fieldBindings[$fName] = $tName
+ # Check params for non-trivial expressions
+ if ($tplIndex.ContainsKey($tName)) {
+ $content = Get-TemplateContent $tplIndex[$tName]
+ foreach ($nt in $content.nonTrivial) {
+ [void]$fieldNonTrivial.Add(@{ field = $fName; template = $tName; name = $nt.name; expression = $nt.expression })
+ }
+ }
+ }
+ }
+
+ $totalTpl = $tplIndex.Count
+ $fieldCount = $fieldBindings.Count
+ $groupBindCount = 0
+ foreach ($k in $groupBindings.Keys) { $groupBindCount += $groupBindings[$k].Count }
+
+ if (-not $Name) {
+ # --- MAP mode ---
+ $lines.Add("=== Templates ($totalTpl defined: $fieldCount field, $groupBindCount group) ===")
+
+ # Field bindings
+ if ($fieldBindings.Count -gt 0) {
+ $lines.Add("")
+ if ($fieldNonTrivial.Count -eq 0) {
+ $fieldNames = @($fieldBindings.Keys)
+ if ($fieldNames.Count -le 8) {
+ $lines.Add("Field bindings ($fieldCount): $($fieldNames -join ', ') (all trivial)")
+ } else {
+ $lines.Add("Field bindings ($fieldCount): (all trivial)")
+ $lines.Add(" $($fieldNames[0..7] -join ', '), ...")
+ }
+ } else {
+ $trivialCount = $fieldBindings.Count - ($fieldNonTrivial | Select-Object -ExpandProperty field -Unique).Count
+ $lines.Add("Field bindings ($fieldCount, $trivialCount trivial):")
+ foreach ($nt in $fieldNonTrivial) {
+ $lines.Add(" $($nt.field): $($nt.name) = $($nt.expression)")
+ }
+ }
+ }
+
+ # Group bindings
+ if ($groupBindings.Count -gt 0) {
+ $lines.Add("")
+ $lines.Add("Group bindings ($groupBindCount):")
+ foreach ($gName in $groupBindings.Keys) {
+ $bindings = $groupBindings[$gName]
+ $parts = @()
+ foreach ($b in $bindings) {
+ $info = "$($b.type) -> $($b.tplName)"
+ if ($tplIndex.ContainsKey($b.tplName)) {
+ $content = Get-TemplateContent $tplIndex[$b.tplName]
+ # Check if any cell has content
+ $hasContent = $false
+ foreach ($r in $content.cells) {
+ foreach ($c in $r.cells) {
+ if ($c -ne "(empty)") { $hasContent = $true; break }
+ }
+ if ($hasContent) { break }
+ }
+ $info += " ($($content.rows) rows"
+ if ($content.params.Count -gt 0) { $info += ", $($content.params.Count) params" }
+ $info += ")"
+ if (-not $hasContent -and $content.params.Count -eq 0) {
+ $info += " spacer"
+ }
+ if ($content.nonTrivial.Count -gt 0) {
+ $ntNames = ($content.nonTrivial | ForEach-Object { $_.name }) -join ', '
+ $info += " *$ntNames"
+ }
+ }
+ $parts += $info
+ }
+ $lines.Add(" $gName")
+ foreach ($p in $parts) {
+ $lines.Add(" $p")
+ }
+ }
+ }
+
+ if ($fieldBindings.Count -gt 0 -or $groupBindings.Count -gt 0) {
+ $lines.Add("")
+ $lines.Add("Use -Name for template details.")
+ }
+ } else {
+ # --- DETAIL mode ---
+ $found = $false
+
+ # Check group bindings first
+ if ($groupBindings.Contains($Name)) {
+ $found = $true
+ $bindings = $groupBindings[$Name]
+ $lines.Add("=== Templates: $Name ===")
+ foreach ($b in $bindings) {
+ $lines.Add("")
+ $tName = $b.tplName
+ if (-not $tplIndex.ContainsKey($tName)) {
+ $lines.Add("$($b.type) -> $tName (template not found)")
+ continue
+ }
+ $content = Get-TemplateContent $tplIndex[$tName]
+ $cellCount = 0
+ foreach ($r in $content.cells) { $cellCount += $r.cells.Count }
+ $lines.Add("$($b.type) -> $tName [$($content.rows) rows, $cellCount cells]:")
+
+ foreach ($r in $content.cells) {
+ $cellStr = $r.cells -join " | "
+ $lines.Add(" Row $($r.row): $cellStr")
+ }
+
+ if ($content.nonTrivial.Count -gt 0) {
+ $lines.Add(" Params:")
+ foreach ($nt in $content.nonTrivial) {
+ $lines.Add(" $($nt.name) = $($nt.expression)")
+ }
+ }
+ }
+ }
+
+ # Check field bindings
+ if ($fieldBindings.Contains($Name)) {
+ if ($found) { $lines.Add("") }
+ $found = $true
+ $tName = $fieldBindings[$Name]
+ $lines.Add("=== Field template: $Name -> $tName ===")
+ if ($tplIndex.ContainsKey($tName)) {
+ $content = Get-TemplateContent $tplIndex[$tName]
+ $cellCount = 0
+ foreach ($r in $content.cells) { $cellCount += $r.cells.Count }
+ $lines.Add("[$($content.rows) rows, $cellCount cells]")
+
+ foreach ($r in $content.cells) {
+ $cellStr = $r.cells -join " | "
+ $lines.Add(" Row $($r.row): $cellStr")
+ }
+
+ if ($content.nonTrivial.Count -gt 0) {
+ $lines.Add(" Non-trivial params:")
+ foreach ($nt in $content.nonTrivial) {
+ $lines.Add(" $($nt.name) = $($nt.expression)")
+ }
+ } else {
+ $lines.Add(" (all params trivial)")
+ }
+ }
+ }
+
+ if (-not $found) {
+ Write-Error "Group or field '$Name' not found in template bindings"
+ exit 1
+ }
+ }
+}
+
+# --- Output ---
+
+$result = $lines.ToArray()
+$totalLines = $result.Count
+
+# OutFile
+if ($OutFile) {
+ $utf8Bom = New-Object System.Text.UTF8Encoding($true)
+ [System.IO.File]::WriteAllLines((Join-Path (Get-Location) $OutFile), $result, $utf8Bom)
+ Write-Host "Written $totalLines lines to $OutFile"
+ exit 0
+}
+
+# Pagination
+if ($Offset -gt 0) {
+ if ($Offset -ge $totalLines) {
+ Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show."
+ exit 0
+ }
+ $result = $result[$Offset..($totalLines - 1)]
+}
+
+if ($result.Count -gt $Limit) {
+ $shown = $result[0..($Limit - 1)]
+ foreach ($l in $shown) { Write-Host $l }
+ Write-Host ""
+ Write-Host "[TRUNCATED] Shown $Limit of $totalLines lines. Use -Offset $($Offset + $Limit) to continue."
+} else {
+ foreach ($l in $result) { Write-Host $l }
+}
diff --git a/.claude/skills/skd-validate/SKILL.md b/.claude/skills/skd-validate/SKILL.md
new file mode 100644
index 00000000..eeba9eb7
--- /dev/null
+++ b/.claude/skills/skd-validate/SKILL.md
@@ -0,0 +1,74 @@
+---
+name: skd-validate
+description: Валидация структурной корректности схемы компоновки данных 1С (СКД) — Template.xml
+argument-hint: [-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 — генерация XML
+/skd-validate — проверка результата
+/skd-info — визуальная сводка
+```
diff --git a/.claude/skills/skd-validate/scripts/skd-validate.ps1 b/.claude/skills/skd-validate/scripts/skd-validate.ps1
new file mode 100644
index 00000000..2baa86c0
--- /dev/null
+++ b/.claude/skills/skd-validate/scripts/skd-validate.ps1
@@ -0,0 +1,719 @@
+# skd-validate v1.0 — Validate 1C DCS structure
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+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 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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..24af5d17
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025-2026 Nick Shirokov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index a1adf052..e29da5fe 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# 1C Skills for Claude Code
+> **Work in progress** — проект в стадии активной разработки. Набор навыков и операций расширяется.
+
Набор [Claude Code Skills](https://docs.anthropic.com/en/docs/claude-code/skills) для работы с артефактами 1С:Предприятия 8.3. Позволяет создавать и модифицировать обработки, макеты печатных форм и другие объекты из XML-исходников, не запоминая детали формата.
## Быстрый старт
@@ -23,6 +25,7 @@
| Табличный документ (MXL) | 4 навыка `/mxl-*` | Анализ, создание, компиляция макетов печатных форм | [Подробнее](docs/mxl-guide.md) |
| Управляемые формы (Form) | 6 навыков `/form-*` | Создание, анализ, генерация, модификация, валидация управляемых форм | [Подробнее](docs/form-guide.md) |
| Роли (Role) | 3 навыка `/role-*` | Анализ прав роли, создание из JSON DSL, валидация | [Подробнее](docs/role-guide.md) |
+| Схема компоновки (СКД) | 4 навыка `/skd-*` | Анализ, генерация из JSON DSL, точечное редактирование, валидация схем компоновки данных | [Подробнее](docs/skd-guide.md) |
| Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — |
## Требования
@@ -41,6 +44,8 @@
- [Form DSL](docs/form-dsl-spec.md) — JSON-формат описания формы для `/form-compile`
- [Роли (Rights.xml)](docs/1c-role-spec.md) — XML-формат прав роли, типы объектов, RLS
- [Role DSL](docs/role-dsl-spec.md) — JSON-формат описания ролей для `/role-compile`
+- [Схема компоновки данных (DCS)](docs/1c-dcs-spec.md) — XML-формат DataCompositionSchema, 930 схем проанализировано
+- [SKD DSL](docs/skd-dsl-spec.md) — JSON-формат описания СКД для `/skd-compile`
## Структура репозитория
@@ -69,12 +74,17 @@
├── role-info/ # Анализ прав роли
├── role-compile/ # Создание роли из JSON DSL
├── role-validate/ # Валидация роли
+├── skd-info/ # Анализ схемы компоновки данных
+├── skd-compile/ # Компиляция СКД из JSON DSL
+├── skd-edit/ # Точечное редактирование СКД (25 операций)
+├── skd-validate/ # Валидация СКД
└── img-grid/ # Сетка для анализа изображений
docs/
├── epf-guide.md # Гайд: внешние обработки
├── mxl-guide.md # Гайд: табличный документ
├── form-guide.md # Гайд: управляемые формы
├── role-guide.md # Гайд: роли
+├── skd-guide.md # Гайд: схема компоновки данных
├── 1c-xml-format-spec.md # Спецификация XML-формата
├── 1c-form-spec.md # Спецификация управляемых форм
├── 1c-help-spec.md # Спецификация встроенной справки
@@ -82,5 +92,8 @@ docs/
├── 1c-spreadsheet-spec.md # Спецификация табличного документа
├── mxl-dsl-spec.md # Спецификация MXL DSL
├── form-dsl-spec.md # Спецификация Form DSL
-└── 1c-role-spec.md # Спецификация ролей (Rights.xml)
+├── 1c-role-spec.md # Спецификация ролей (Rights.xml)
+├── 1c-dcs-spec.md # Спецификация СКД (DataCompositionSchema)
+├── skd-dsl-spec.md # Спецификация SKD DSL
+└── role-dsl-spec.md # Спецификация Role DSL
```
diff --git a/docs/1c-dcs-spec.md b/docs/1c-dcs-spec.md
index 6c07c0a3..cb0ca398 100644
--- a/docs/1c-dcs-spec.md
+++ b/docs/1c-dcs-spec.md
@@ -299,7 +299,8 @@ DataCompositionSchema
| `dcscom:accountTypeExpression` | Выражение для определения типа счёта |
| `dcscom:balance` | Поле является остатком |
| `dcscom:balanceGroup` | Группа остатка |
-| `dcscom:period` | Поле — период |
+| `dcscom:periodNumber` | Номер периода (обычно `1`) |
+| `dcscom:periodType` | Тип периода (`Main`, `Additional`) |
### 4.7. Тип значения (valueType)
@@ -313,7 +314,15 @@ DataCompositionSchema
```
-Типы: `xs:string`, `xs:dateTime`, `xs:decimal`, `xs:boolean`, ссылочные (`d4p1:CatalogRef.Номенклатура`).
+Типы: `xs:string`, `xs:dateTime`, `xs:decimal`, `xs:boolean`, ссылочные типы конфигурации.
+
+Ссылочные типы объявляются с inline namespace на элементе ``:
+
+```xml
+d5p1:CatalogRef.Номенклатура
+```
+
+Префикс (`d5p1`, `d4p1` и т.д.) — автогенерируемый, суть в URI `http://v8.1c.ru/8.1/data/enterprise/current-config`. Поддерживаются: `CatalogRef`, `DocumentRef`, `EnumRef`, `ChartOfAccountsRef`, `ChartOfCharacteristicTypesRef` и др.
Квалификаторы:
- `v8:StringQualifiers` → `v8:Length`, `v8:AllowedLength` (Fixed/Variable)
@@ -446,7 +455,28 @@ DataCompositionSchema
|---|---|---|
| `dataPath` | да | Путь к полю |
| `expression` | да | Агрегатная функция: `Сумма(...)`, `Количество(...)`, `Максимум(...)`, `Минимум(...)`, `Среднее(...)` |
-| `group` | нет | Для какой группировки считать итоги |
+| `group` | нет | Имя группировки, для которой считать итоги. Без `group` — для всех группировок |
+
+### Разные формулы для разных группировок
+
+Одно поле может иметь несколько записей `totalField` с разными формулами для разных группировок:
+
+```xml
+
+
+ ПравоИнтерактивное
+ Максимум(ПравоИнтерактивное)
+ ОбъектМетаданных
+
+
+
+ ПравоИнтерактивное
+ Максимум(ПравоОтчета)
+ Отчет
+
+```
+
+Это позволяет вычислять ресурс по-разному в зависимости от контекста группировки.
---
@@ -494,7 +524,7 @@ DataCompositionSchema
| Дата | `xs:dateTime` | `0001-01-01T00:00:00` |
| Строка | `xs:string` | `Т13` |
| Стандартный период | `v8:StandardPeriod` | `LastMonth` |
-| Ссылка | `d4p1:CatalogRef.ИмяСправочника` | `xsi:nil="true"` |
+| Ссылка | `d5p1:CatalogRef.ИмяСправочника` (с `xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config"`) | `xsi:nil="true"` |
| null | — | `xsi:nil="true"` |
Стандартные варианты периодов (`v8:StandardPeriodVariant`): `Custom`, `Today`, `ThisWeek`, `ThisMonth`, `ThisQuarter`, `ThisYear`, `LastMonth`, `LastQuarter`, `LastYear` и др.
diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md
new file mode 100644
index 00000000..9a970d1d
--- /dev/null
+++ b/docs/skd-dsl-spec.md
@@ -0,0 +1,790 @@
+# JSON DSL для схемы компоновки данных (СКД)
+
+Компактный JSON-формат для описания `DataCompositionSchema` (Template.xml).
+Компилируется навыком `/skd-compile` в XML, валидируется `/skd-validate`.
+
+---
+
+## 1. Корневая структура
+
+```json
+{
+ "dataSources": [...],
+ "dataSets": [...],
+ "dataSetLinks": [...],
+ "calculatedFields": [...],
+ "totalFields": [...],
+ "parameters": [...],
+ "templates": [...],
+ "groupTemplates": [...],
+ "settingsVariants": [...]
+}
+```
+
+**Умолчания:**
+- `dataSources` опущен → авто-создаётся `{ "name": "ИсточникДанных1", "type": "Local" }`
+- `source` в наборе опущен → первый dataSource
+- `name` набора опущен → "НаборДанных1", "НаборДанных2"...
+- `settingsVariants` опущен → один вариант "Основной" с детальной группировкой и `selection: ["Auto"]`
+
+---
+
+## 2. Источники данных (dataSources)
+
+```json
+"dataSources": [
+ { "name": "ИсточникДанных1", "type": "Local" }
+]
+```
+
+| Поле | Обязат. | Умолчание | XML-маппинг |
+|------|---------|-----------|-------------|
+| `name` | да | — | `` |
+| `type` | нет | `"Local"` | `` |
+
+Значения `type`: `"Local"`, `"External"`.
+
+---
+
+## 3. Наборы данных (dataSets)
+
+Тип определяется по ключу-дискриминатору:
+
+| Ключ | Тип | xsi:type |
+|------|-----|----------|
+| `query` | Запрос | `DataSetQuery` |
+| `objectName` | Объект | `DataSetObject` |
+| `items` | Объединение | `DataSetUnion` |
+
+### DataSetQuery (самый частый)
+
+```json
+{ "name": "Продажи", "query": "ВЫБРАТЬ ...", "fields": [...], "autoFillFields": false }
+```
+
+### DataSetObject
+
+```json
+{ "name": "ТаблицаПроверки", "objectName": "ТаблицаПроверки", "fields": [...] }
+```
+
+### DataSetUnion
+
+```json
+{
+ "name": "Объединение",
+ "items": [
+ { "name": "Набор1", "query": "...", "fields": [...] },
+ { "name": "Набор2", "query": "...", "fields": [...] }
+ ],
+ "fields": [...]
+}
+```
+
+| Поле | Обязат. | Описание |
+|------|---------|----------|
+| `name` | нет | Авто: "НаборДанных1"... |
+| `source` | нет | Имя dataSource (авто: первый) |
+| `query` | да* | Текст запроса (DataSetQuery) |
+| `objectName` | да* | Имя объекта (DataSetObject) |
+| `items` | да* | Вложенные наборы (DataSetUnion) |
+| `fields` | нет | Массив полей |
+| `autoFillFields` | нет | `false` — отключить автозаполнение (по умолчанию не выводится = true) |
+
+---
+
+## 4. Поля — shorthand и объектная форма
+
+### Shorthand-строка
+
+```
+"[: ] [@role...] [#restrict...]"
+```
+
+Примеры:
+
+```json
+"fields": [
+ "Наименование",
+ "Количество: decimal(15,2)",
+ "Организация: CatalogRef.Организации @dimension",
+ "Служебное: string #noFilter #noOrder",
+ "Счёт: CatalogRef.Хозрасчетный @account",
+ "Сумма: decimal(15,2) @balance"
+]
+```
+
+### Объектная форма
+
+```json
+{
+ "dataPath": "Сумма",
+ "field": "Сумма",
+ "title": "Сумма продаж",
+ "type": "decimal(15,2)",
+ "role": { "dimension": true },
+ "restrict": ["noFilter", "noGroup"],
+ "attrRestrict": ["noFilter"],
+ "appearance": { "Формат": "ЧДЦ=2" },
+ "presentationExpression": "Формат(Сумма, \"ЧДЦ=2\")"
+}
+```
+
+### Парсинг shorthand
+
+1. Разделить по пробелам; найти `@`-роли и `#`-ограничения
+2. Остаток до первого `:` — `dataPath` (и `field` по умолчанию)
+3. После `:` до `@`/`#` — тип
+
+### Типы
+
+| DSL | XML v8:Type | Квалификатор |
+|-----|-------------|--------------|
+| `string` | `xs:string` | Length=0, AllowedLength=Variable |
+| `string(N)` | `xs:string` | Length=N, AllowedLength=Variable |
+| `decimal(D,F)` | `xs:decimal` | Digits=D, FractionDigits=F, AllowedSign=Any |
+| `decimal(D,F,nonneg)` | `xs:decimal` | Digits=D, FractionDigits=F, AllowedSign=Nonnegative |
+| `boolean` | `xs:boolean` | — |
+| `date` | `xs:dateTime` | DateFractions=Date |
+| `dateTime` | `xs:dateTime` | DateFractions=DateTime |
+| `CatalogRef.XXX` | `d5p1:CatalogRef.XXX` | inline xmlns:d5p1 |
+| `DocumentRef.XXX` | `d5p1:DocumentRef.XXX` | inline xmlns:d5p1 |
+| `EnumRef.XXX` | `d5p1:EnumRef.XXX` | inline xmlns:d5p1 |
+| `ChartOfAccountsRef.XXX` | `d5p1:ChartOfAccountsRef.XXX` | inline xmlns:d5p1 |
+| `StandardPeriod` | `v8:StandardPeriod` | — |
+
+> **Ссылочные типы** (`CatalogRef.XXX`, `DocumentRef.XXX` и др.) эмитируются с inline namespace declaration: `d5p1:CatalogRef.XXX`. Использование префикса `cfg:` вместо `d5p1:` с объявлением namespace приводит к ошибке XDTO. Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией (не пустую).
+
+### Синонимы типов
+
+Все имена типов регистронезависимые. Поддерживаются русские и альтернативные имена:
+
+| Синоним | Канонический тип |
+|---------|-----------------|
+| `число`, `Число` | `decimal` |
+| `строка`, `Строка` | `string` |
+| `булево`, `Булево`, `bool` | `boolean` |
+| `дата`, `Дата` | `date` |
+| `датаВремя`, `ДатаВремя` | `dateTime` |
+| `СтандартныйПериод` | `StandardPeriod` |
+| `int`, `integer`, `number`, `num` | `decimal` |
+| `СправочникСсылка.XXX` | `CatalogRef.XXX` |
+| `ДокументСсылка.XXX` | `DocumentRef.XXX` |
+| `ПеречислениеСсылка.XXX` | `EnumRef.XXX` |
+| `ПланСчетовСсылка.XXX` | `ChartOfAccountsRef.XXX` |
+| `ПланВидовХарактеристикСсылка.XXX` | `ChartOfCharacteristicTypesRef.XXX` |
+
+Параметризованные: `число(15,2)` → `decimal(15,2)`, `строка(100)` → `string(100)`.
+
+### Роли
+
+| DSL shorthand | Объектная форма | XML |
+|---------------|----------------|-----|
+| `@dimension` | `"role": "dimension"` или `{"dimension": true}` | `true` |
+| `@account` | `"role": "account"` или `{"account": true}` | `true` |
+| `@balance` | `"role": "balance"` или `{"balance": true}` | `true` |
+| `@period` | `"role": "period"` или `{"period": true}` | `1` + `Main` |
+
+Объектная форма с доп. полями:
+```json
+"role": {
+ "account": true,
+ "accountTypeExpression": "Счёт.ВидСчёта",
+ "balanceGroup": "/Остатки"
+}
+```
+
+### Ограничения
+
+| DSL shorthand | Объектная форма | XML useRestriction |
+|---------------|----------------|-----|
+| `#noField` | `"noField"` | `true` |
+| `#noFilter` / `#noCondition` | `"noFilter"` | `true` |
+| `#noGroup` | `"noGroup"` | `true` |
+| `#noOrder` | `"noOrder"` | `true` |
+
+### Оформление (appearance)
+
+```json
+"appearance": {
+ "Формат": "ЧДЦ=2",
+ "ГоризонтальноеПоложение": "Center"
+}
+```
+
+Маппинг на XML:
+```xml
+
+
+ Формат
+ ЧДЦ=2
+
+
+```
+
+Значения `ГоризонтальноеПоложение` → `xsi:type="v8ui:HorizontalAlign"`.
+
+---
+
+## 5. Итоговые поля (totalFields)
+
+### Shorthand
+
+```
+": <Функция>"
+": <Функция>(<выражение>)"
+```
+
+Примеры:
+
+```json
+"totalFields": [
+ "Количество: Сумма",
+ "Цена: Максимум",
+ "Стоимость: Сумма(Кол * Цена)"
+]
+```
+
+**Парсинг:** `"A: Func"` → `dataPath=A`, `expression=Func(A)`. `"A: Func(expr)"` → `dataPath=A`, `expression=Func(expr)`.
+
+Функции (русские): `Сумма`, `Количество`, `Максимум`, `Минимум`, `Среднее`.
+
+### Объектная форма
+
+```json
+{ "dataPath": "X", "expression": "Максимум(X)", "group": "Группа1" }
+```
+
+### Привязка к группировкам (group)
+
+В объектной форме поле `group` может быть строкой или массивом строк. Каждая строка задаёт имя группировки, для которой вычисляется итог:
+
+```json
+"totalFields": [
+ { "dataPath": "Кол", "expression": "Сумма(Кол)", "group": ["ГруппаПользователей", "ГруппаПользователей Иерархия", "ОбщийИтог"] }
+]
+```
+
+XML-маппинг — по `` на каждый элемент:
+```xml
+
+ Кол
+ Сумма(Кол)
+ ГруппаПользователей
+ ГруппаПользователей Иерархия
+ ОбщийИтог
+
+```
+
+---
+
+## 6. Параметры (parameters)
+
+### Shorthand
+
+```
+": [= ] [@autoDates]"
+```
+
+Примеры:
+
+```json
+"parameters": [
+ "Период: StandardPeriod = LastMonth @autoDates",
+ "Организация: CatalogRef.Организации",
+ "ДатаОтчета: date"
+]
+```
+
+**Парсинг:** `"A: T = V"` → `name=A`, `type=T`, `value=V`. Значение `LastMonth` и другие варианты периодов → `v8:StandardPeriod` с `v8:variant`.
+
+### @autoDates
+
+Флаг `@autoDates` в shorthand параметра автоматически генерирует два дополнительных параметра:
+- `ДатаНачала` (date, expression=`&<Имя>.ДатаНачала`, availableAsField=false)
+- `ДатаОкончания` (date, expression=`&<Имя>.ДатаОкончания`, availableAsField=false)
+
+Заменяет типовой бойлерплейт из 5 строк на 1:
+
+```json
+// Было:
+"parameters": [
+ "Период: StandardPeriod = LastMonth",
+ { "name": "ДатаНачала", "type": "date", "expression": "&Период.ДатаНачала", "availableAsField": false },
+ { "name": "ДатаОкончания", "type": "date", "expression": "&Период.ДатаОкончания", "availableAsField": false }
+]
+
+// Стало:
+"parameters": ["Период: StandardPeriod = LastMonth @autoDates"]
+```
+
+### Объектная форма
+
+```json
+{
+ "name": "ДатаНач",
+ "title": "Дата начала",
+ "type": "date",
+ "value": "0001-01-01T00:00:00",
+ "expression": "&Период.ДатаНачала",
+ "availableAsField": false,
+ "useRestriction": true,
+ "use": "Always"
+}
+```
+
+| Поле | Описание |
+|------|----------|
+| `name` | Имя параметра |
+| `title` | Заголовок (умолч. = name) |
+| `type` | Тип (см. таблицу типов) |
+| `value` | Значение по умолчанию |
+| `expression` | Выражение для вычисления |
+| `availableAsField` | `false` — скрыть из полей |
+| `useRestriction` | `true` — скрыть от пользователя |
+| `use` | `"Always"`, `"Auto"` |
+
+### Значения параметров по типу
+
+| Тип | value | XML |
+|-----|-------|-----|
+| `StandardPeriod` | `"LastMonth"`, `"ThisYear"` и др. | `LastMonth` |
+| `date` | `"0001-01-01T00:00:00"` | `xsi:type="xs:dateTime"` |
+| `string` | `"текст"` | `xsi:type="xs:string"` |
+| `boolean` | `true`/`false` | `xsi:type="xs:boolean"` |
+
+Стандартные варианты периодов: `Custom`, `Today`, `ThisWeek`, `ThisMonth`, `ThisQuarter`, `ThisYear`, `LastMonth`, `LastQuarter`, `LastYear`.
+
+---
+
+## 7. Вычисляемые поля (calculatedFields)
+
+### Shorthand
+
+```
+" = "
+```
+
+```json
+"calculatedFields": [
+ "УИД = Строка(Ссылка.УникальныйИдентификатор())",
+ "Итого = Количество * Цена"
+]
+```
+
+### Объектная форма
+
+```json
+{
+ "dataPath": "Итого",
+ "expression": "Количество * Цена",
+ "title": "Итого",
+ "type": "decimal(15,2)",
+ "restrict": ["noGroup"],
+ "appearance": { "Формат": "ЧДЦ=2" }
+}
+```
+
+---
+
+## 8. Связи наборов (dataSetLinks)
+
+Только объектная форма:
+
+```json
+"dataSetLinks": [
+ {
+ "source": "Периоды",
+ "dest": "Данные",
+ "sourceExpr": "Месяц",
+ "destExpr": "Месяц",
+ "parameter": "НачалоМесяца"
+ }
+]
+```
+
+| Поле | XML |
+|------|-----|
+| `source` | `` |
+| `dest` | `` |
+| `sourceExpr` | `` |
+| `destExpr` | `` |
+| `parameter` | `` (опц.) |
+
+---
+
+## 9. Варианты настроек (settingsVariants)
+
+```json
+"settingsVariants": [{
+ "name": "Основной",
+ "presentation": "Основной вариант",
+ "settings": {
+ "selection": [...],
+ "filter": [...],
+ "order": [...],
+ "conditionalAppearance": [...],
+ "outputParameters": {...},
+ "dataParameters": [...],
+ "structure": [...]
+ }
+}]
+```
+
+### selection
+
+```json
+"selection": [
+ "Наименование",
+ { "field": "Количество", "title": "Кол-во" },
+ "Auto"
+]
+```
+
+- Строка → `SelectedItemField`
+- `"Auto"` → `SelectedItemAuto` (только на уровне группировок; на верхнем уровне settings игнорируется)
+- Объект с `field`/`title` → `SelectedItemField` с `lwsTitle`
+
+### filter
+
+#### Shorthand-строка
+
+```json
+"filter": [
+ "Организация = _ @off @user",
+ "Дата >= 2024-01-01T00:00:00",
+ "Статус filled",
+ "Количество > 0"
+]
+```
+
+Формат: `"<Поле> <оператор> [<значение>] [@off] [@user] [@quickAccess] [@normal] [@inaccessible]"`.
+
+- Значение `_` — пустое (placeholder, не выводится в XML)
+- `@off` → `use=false`
+- `@user` → `userSettingID=auto` (генерировать GUID)
+- `@quickAccess` → `viewMode=QuickAccess`
+- `@normal` → `viewMode=Normal`
+- `@inaccessible` → `viewMode=Inaccessible`
+- Типы значений автоопределяются: `true`/`false` → boolean, `2024-01-01T00:00:00` → dateTime, числа → decimal, прочее → string
+
+#### Объектная форма
+
+```json
+"filter": [
+ { "field": "Организация", "op": "=", "use": false, "userSettingID": "auto" },
+ { "field": "Дата", "op": ">=", "value": "0001-01-01T00:00:00", "valueType": "xs:dateTime" },
+ { "group": "Or", "items": [
+ { "field": "Статус", "op": "=", "value": true, "valueType": "xs:boolean" },
+ { "field": "Пометка", "op": "filled" }
+ ]}
+]
+```
+
+| Поле | Описание |
+|------|----------|
+| `field` | Имя поля |
+| `op` | Оператор (см. таблицу) |
+| `value` | Правая часть (опц.) |
+| `valueType` | xsi:type для значения (опц.) |
+| `use` | Включён (`true` по умолчанию) |
+| `presentation` | Текст подсказки |
+| `viewMode` | `"Normal"`, `"QuickAccess"`, `"Inaccessible"` |
+| `userSettingID` | `"auto"` → генерировать GUID |
+| `userSettingPresentation` | Отображаемое имя настройки (LocalStringType) |
+
+Операторы:
+
+| DSL | XML comparisonType |
+|-----|--------------------|
+| `=` | `Equal` |
+| `<>` | `NotEqual` |
+| `>` | `Greater` |
+| `>=` | `GreaterOrEqual` |
+| `<` | `Less` |
+| `<=` | `LessOrEqual` |
+| `in` | `InList` |
+| `notIn` | `NotInList` |
+| `inHierarchy` | `InHierarchy` |
+| `contains` | `Contains` |
+| `notContains` | `NotContains` |
+| `beginsWith` | `BeginsWith` |
+| `filled` | `Filled` |
+| `notFilled` | `NotFilled` |
+
+Группа условий: `{ "group": "And"|"Or"|"Not", "items": [...] }` → `FilterItemGroup` с `groupType`.
+
+### order
+
+```json
+"order": ["Количество desc", "Наименование", "Auto"]
+```
+
+- `"Field"` → `OrderItemField`, `orderType=Asc`
+- `"Field desc"` → `OrderItemField`, `orderType=Desc`
+- `"Field asc"` → `OrderItemField`, `orderType=Asc`
+- `"Auto"` → `OrderItemAuto` (только на уровне группировок; на верхнем уровне settings игнорируется)
+
+### conditionalAppearance
+
+Условное оформление — массив правил, каждое задаёт набор полей (selection), условия (filter), параметры оформления (appearance) и мета-атрибуты.
+
+```json
+"conditionalAppearance": [
+ {
+ "selection": ["Сумма"],
+ "filter": ["Сумма > 1000"],
+ "appearance": { "ЦветТекста": "style:ПросроченныеДанныеЦвет" },
+ "presentation": "Выделять крупные суммы",
+ "viewMode": "Normal",
+ "userSettingID": "auto"
+ },
+ {
+ "filter": ["Статус notFilled"],
+ "appearance": { "Текст": "Не указано", "ЦветТекста": "web:Gray" },
+ "presentation": "Скрывать пустые статусы"
+ }
+]
+```
+
+| Поле | Описание |
+|------|----------|
+| `selection` | Массив полей, к которым применяется. Пусто/опущено = все поля |
+| `filter` | Условия (shorthand-строки или объекты, как в settings filter) |
+| `appearance` | Объект `{ "Параметр": "Значение" }` |
+| `presentation` | Описание правила |
+| `use` | Включено (`true` по умолчанию) |
+| `viewMode` | `"Normal"`, `"QuickAccess"`, `"Inaccessible"` |
+| `userSettingID` | `"auto"` → генерировать GUID |
+
+**Типы значений appearance** определяются автоматически:
+- `style:XXX`, `web:XXX`, `win:XXX` → `v8ui:Color`
+- `true`/`false` → `xs:boolean`
+- Параметр `Текст` или `Заголовок` → `v8:LocalStringType`
+- Прочее → `xs:string`
+
+Поддержка `use=false` на уровне параметра:
+```json
+"appearance": {
+ "ЦветФона": { "value": "web:LightGray", "use": false }
+}
+```
+
+### outputParameters
+
+```json
+"outputParameters": {
+ "Заголовок": "Мой отчёт",
+ "ВыводитьЗаголовок": "Output",
+ "МакетОформления": "ОформлениеОтчетовЧерноБелый"
+}
+```
+
+Ключ → `dcscor:parameter`, значение → `dcscor:value`.
+
+Типы значений определяются автоматически:
+- `"Заголовок"` → `v8:LocalStringType`
+- `"ВыводитьЗаголовок"`, `"ВыводитьПараметрыДанных"`, `"ВыводитьОтбор"` → `dcsset:DataCompositionTextOutputType`
+- `"РасположениеПолейГруппировки"` → `dcsset:DataCompositionGroupFieldsPlacement`
+- `"РасположениеРеквизитов"` → `dcsset:DataCompositionAttributesPlacement`
+- `"ГоризонтальноеРасположениеОбщихИтогов"`, `"ВертикальноеРасположениеОбщихИтогов"` → `dcscor:DataCompositionTotalPlacement`
+- Прочие → `xs:string`
+
+### dataParameters
+
+#### Shorthand-строка
+
+```json
+"dataParameters": [
+ "Период = LastMonth @user",
+ "Организация @off @user"
+]
+```
+
+Формат: `"<Имя> [= <значение>] [@off] [@user] [@quickAccess] [@normal] [@inaccessible]"`.
+
+- Значения-варианты периодов (`LastMonth`, `ThisYear` и др.) автоматически оборачиваются в `v8:StandardPeriod`
+- `@off` → `use=false`, `@user` → `userSettingID=auto`
+
+#### Объектная форма
+
+```json
+"dataParameters": [
+ { "parameter": "Период", "value": { "variant": "LastMonth" }, "userSettingID": "auto" },
+ { "parameter": "Организация", "use": false, "viewMode": "Normal", "userSettingID": "auto", "userSettingPresentation": "Организация отчёта" }
+]
+```
+
+| Поле | Описание |
+|------|----------|
+| `parameter` | Имя параметра |
+| `value` | Значение (объект `{ "variant": "LastMonth" }` для StandardPeriod, или скаляр) |
+| `use` | Включён (`true` по умолчанию) |
+| `viewMode` | `"Normal"`, `"QuickAccess"`, `"Inaccessible"` |
+| `userSettingID` | `"auto"` → генерировать GUID |
+| `userSettingPresentation` | Отображаемое имя настройки (LocalStringType) |
+
+### structure
+
+#### String shorthand (рекомендуется для типичных случаев)
+
+```json
+"structure": "Организация > details"
+"structure": "Организация > Номенклатура > details"
+"structure": "Период > Организация > Номенклатура > details"
+```
+
+`>` разделяет уровни вложенности. Каждый сегмент — группировка по указанному полю. `details` (или `детали`) — детальные записи (пустой `groupBy`). Для каждого уровня `selection` и `order` автоматически `["Auto"]`.
+
+#### Массив объектов
+
+```json
+"structure": [
+ {
+ "type": "group",
+ "groupBy": ["Организация"],
+ "children": [
+ { "type": "group" }
+ ]
+ }
+]
+```
+
+**Умолчания:** `selection` и `order` по умолчанию `["Auto"]` на каждом уровне (в группировках, строках/колонках таблиц, точках/сериях диаграмм). Указывать явно нужно только если требуется другой набор полей.
+
+#### Группировка (group)
+
+| Поле | Описание |
+|------|----------|
+| `type` | `"group"` |
+| `name` | Имя группировки (опц.) |
+| `groupBy` | Массив полей. Пусто/опущено = детальные записи |
+| `groupType` | `"Items"` (умолч.), `"Hierarchy"`, `"HierarchyOnly"` |
+| `selection` | Выборка (умолч. `["Auto"]`) |
+| `filter` | Отборы (как в settings) |
+| `order` | Сортировка (умолч. `["Auto"]`) |
+| `outputParameters` | Параметры вывода (как в settings) |
+| `children` | Вложенные элементы структуры |
+
+Пустой `groupBy` (или `[]`) = детальные записи (без `groupItems` в XML).
+
+#### Таблица (table)
+
+```json
+{
+ "type": "table",
+ "name": "Таблица",
+ "rows": [
+ { "groupBy": ["Номенклатура"], "selection": ["Auto"], "order": ["Auto"] }
+ ],
+ "columns": [
+ { "groupBy": ["Период"], "selection": ["Auto"], "order": ["Auto"] }
+ ]
+}
+```
+
+#### Диаграмма (chart)
+
+```json
+{
+ "type": "chart",
+ "points": { "groupBy": ["Организация"], "order": ["Auto"] },
+ "series": { "groupBy": ["Месяц"], "order": ["Auto"] },
+ "selection": ["Сумма"]
+}
+```
+
+---
+
+## 10. Макеты и привязки (templates, groupTemplates)
+
+Редко используются. Поддерживаются в объектной форме, близкой к XML.
+
+### templates
+
+```json
+"templates": [
+ {
+ "name": "Макет1",
+ "template": "",
+ "parameters": [
+ { "name": "ТипЦены", "expression": "Представление(ТипЦен)" }
+ ]
+ }
+]
+```
+
+### groupTemplates
+
+```json
+"groupTemplates": [
+ { "groupField": "ТипЦен", "templateType": "Header", "template": "Макет1" }
+]
+```
+
+---
+
+## 11. Полный пример — минимальный
+
+```json
+{
+ "dataSets": [
+ {
+ "name": "НаборДанных1",
+ "query": "ВЫБРАТЬ\n\tНоменклатура.Наименование КАК Наименование,\n\tКОЛИЧЕСТВО(1) КАК Количество\nИЗ\n\tСправочник.Номенклатура КАК Номенклатура\nСГРУППИРОВАТЬ ПО\n\tНоменклатура.Наименование",
+ "fields": [
+ { "dataPath": "Наименование", "title": "Наименование" },
+ "Количество"
+ ]
+ }
+ ],
+ "totalFields": ["Количество: Сумма"],
+ "settingsVariants": [{
+ "name": "Основной",
+ "settings": {
+ "selection": ["Наименование", "Количество"],
+ "structure": [{ "type": "group" }]
+ }
+ }]
+}
+```
+
+## 12. Полный пример — средний (с shorthand v2)
+
+```json
+{
+ "dataSets": [
+ {
+ "name": "Продажи",
+ "query": "ВЫБРАТЬ\n\tПродажи.Организация,\n\tПродажи.Номенклатура,\n\tПродажи.Количество,\n\tПродажи.Сумма\nИЗ\n\tРегистрНакопления.Продажи КАК Продажи\n{ГДЕ\n\tПродажи.Период >= &ДатаНачала\n\tИ Продажи.Период < &ДатаОкончания}",
+ "fields": [
+ "Организация: СправочникСсылка.Организации @dimension",
+ "Номенклатура: СправочникСсылка.Номенклатура @dimension",
+ "Количество: число(15,3)",
+ "Сумма: число(15,2)"
+ ]
+ }
+ ],
+ "totalFields": ["Количество: Сумма", "Сумма: Сумма"],
+ "parameters": [
+ "Период: СтандартныйПериод = LastMonth @autoDates"
+ ],
+ "settingsVariants": [{
+ "name": "Основной",
+ "presentation": "Продажи по организациям",
+ "settings": {
+ "selection": ["Номенклатура", "Количество", "Сумма", "Auto"],
+ "filter": ["Организация = _ @off @user"],
+ "order": ["Сумма desc", "Auto"],
+ "outputParameters": {
+ "Заголовок": "Анализ продаж",
+ "ВыводитьЗаголовок": "Output"
+ },
+ "dataParameters": ["Период = LastMonth @user"],
+ "structure": "Организация > details"
+ }
+ }]
+}
+```
+
+**Сравнение с v1:** средний пример сократился с 58 до 33 строк (−43%). Основная экономия: `@autoDates` (−4 строки), structure shorthand (−9 строк), filter/dataParam shorthand (−4 строки).
diff --git a/docs/skd-guide.md b/docs/skd-guide.md
new file mode 100644
index 00000000..7da5f855
--- /dev/null
+++ b/docs/skd-guide.md
@@ -0,0 +1,177 @@
+# Схема компоновки данных (СКД)
+
+Навыки группы `/skd-*` позволяют анализировать, создавать, редактировать и проверять схемы компоновки данных 1С — XML-файлы DataCompositionSchema (Template.xml).
+
+## Навыки
+
+| Навык | Параметры | Описание |
+|-------|-----------|----------|
+| `/skd-info` | ` [-Mode] [-Name]` | Анализ структуры СКД: наборы, поля, параметры, ресурсы, варианты (10 режимов) |
+| `/skd-compile` | ` ` | Генерация Template.xml из JSON DSL: наборы, поля, итоги, параметры, варианты |
+| `/skd-edit` | ` -Operation -Value ""` | Точечное редактирование: 25 атомарных операций (add/set/modify/clear/remove) |
+| `/skd-validate` | ` [-MaxErrors 20]` | Валидация структурной корректности: ~30 проверок |
+
+## Рабочий цикл
+
+```
+Описание отчёта (текст) → JSON DSL → /skd-compile → Template.xml → /skd-validate
+ ↕ /skd-edit → /skd-info
+```
+
+1. Claude формирует JSON-определение СКД (shorthand-поля, параметры, итоги, варианты)
+2. `/skd-compile` генерирует Template.xml с корректными namespace, типами, группировками
+3. `/skd-edit` вносит точечные изменения: добавление полей, фильтров, наборов данных, вариантов, условного оформления и т.д.
+4. `/skd-validate` проверяет корректность XML
+5. `/skd-info` выводит компактную сводку для визуальной проверки
+
+## JSON DSL — компактный формат
+
+СКД описываются в JSON с двумя уровнями детализации для каждой секции:
+
+### Минимальный пример
+
+```json
+{
+ "dataSets": [{
+ "query": "ВЫБРАТЬ Номенклатура.Наименование ИЗ Справочник.Номенклатура КАК Номенклатура",
+ "fields": ["Наименование"]
+ }]
+}
+```
+
+Умолчания: dataSource создаётся автоматически (`ИсточникДанных1/Local`), набор получает имя `НаборДанных1`, вариант настроек "Основной" с деталями.
+
+### Поля — shorthand
+
+```json
+"fields": [
+ "Наименование",
+ "Количество: decimal(15,2)",
+ "Организация: CatalogRef.Организации @dimension",
+ "Служебное: string #noFilter #noOrder"
+]
+```
+
+Формат: `Имя[: Тип] [@роль...] [#ограничение...]`. Роли: `@dimension`, `@account`, `@balance`, `@period`. Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`.
+
+### Итоги — shorthand
+
+```json
+"totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"]
+```
+
+Формат: `Поле: Функция` или `Поле: Функция(выражение)`. Объектная форма поддерживает привязку к группировкам: `{ "dataPath": "X", "expression": "Сумма(X)", "group": ["Группа1", "ОбщийИтог"] }`.
+
+### Параметры — shorthand + @autoDates
+
+```json
+"parameters": [
+ "Период: StandardPeriod = LastMonth @autoDates",
+ "Организация: CatalogRef.Организации"
+]
+```
+
+`@autoDates` автоматически генерирует параметры `ДатаНачала`/`ДатаОкончания` (заменяет 5 строк на 1).
+
+### Вычисляемые поля — shorthand
+
+```json
+"calculatedFields": ["Итого = Количество * Цена"]
+```
+
+### Варианты настроек — shorthand
+
+```json
+"settingsVariants": [{
+ "name": "Основной",
+ "settings": {
+ "selection": ["Номенклатура", "Количество", "Сумма"],
+ "filter": ["Организация = _ @off @user"],
+ "order": ["Сумма desc"],
+ "dataParameters": ["Период = LastMonth @user"],
+ "outputParameters": { "Заголовок": "Мой отчёт" },
+ "structure": "Организация > details"
+ }
+}]
+```
+
+- **filter shorthand**: `"Поле оператор значение @флаги"` — флаги `@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`
+- **dataParameters shorthand**: `"Имя = значение @флаги"`
+- **structure shorthand**: `"Поле1 > Поле2 > details"` — `>` разделяет уровни группировки
+- **conditionalAppearance**: условное оформление с автоопределением типов значений (Color, Boolean, LocalStringType)
+
+### Объектная форма
+
+Все секции поддерживают полную объектную форму для сложных случаев (title, appearance, role с выражениями, userSettingID, userSettingPresentation, conditionalAppearance, группы фильтров And/Or/Not и т.д.). Подробности — в [спецификации SKD DSL](skd-dsl-spec.md).
+
+## Сценарии использования
+
+### Анализ существующей СКД
+
+```
+> Проанализируй схему компоновки отчёта Reports/АнализНДФЛ/Templates/ОсновнаяСхемаКомпоновкиДанных
+```
+
+Claude вызовет `/skd-info` (overview → trace → query → variant) и опишет:
+- наборы данных и их поля
+- параметры и значения по умолчанию
+- ресурсы и формулы агрегации
+- структуру группировок в вариантах настроек
+
+### Создание СКД по описанию
+
+```
+> Создай СКД для отчёта по продажам: группировка по организациям,
+> поля Номенклатура, Количество, Сумма. Период — параметр.
+```
+
+Claude сформирует JSON:
+```json
+{
+ "dataSets": [{
+ "name": "Продажи",
+ "query": "ВЫБРАТЬ ...",
+ "fields": [
+ "Организация: CatalogRef.Организации @dimension",
+ "Номенклатура: CatalogRef.Номенклатура @dimension",
+ "Количество: decimal(15,3)",
+ "Сумма: decimal(15,2)"
+ ]
+ }],
+ "totalFields": ["Количество: Сумма", "Сумма: Сумма"],
+ "parameters": ["Период: StandardPeriod = LastMonth @autoDates"],
+ "settingsVariants": [{
+ "name": "Основной",
+ "settings": {
+ "selection": ["Номенклатура", "Количество", "Сумма"],
+ "dataParameters": ["Период = LastMonth @user"],
+ "structure": "Организация > details"
+ }
+ }]
+}
+```
+
+И вызовет `/skd-compile` → `/skd-validate` → `/skd-info`.
+
+### Проверка существующей СКД
+
+```
+> Проверь корректность СКД Reports/МойОтчёт/Templates/ОсновнаяСхемаКомпоновкиДанных/Ext/Template.xml
+```
+
+Claude вызовет `/skd-validate` и покажет результат: ошибки (битые ссылки, дубликаты, невалидные типы) и предупреждения.
+
+## Структура файлов СКД
+
+```
+<Объект>/Templates/
+├── ИмяМакета.xml # Метаданные (UUID, TemplateType=DataCompositionSchema)
+└── ИмяМакета/
+ └── Ext/
+ └── Template.xml # Тело схемы (DataCompositionSchema)
+```
+
+## Спецификации
+
+- [1c-dcs-spec.md](1c-dcs-spec.md) — XML-формат DataCompositionSchema, namespace, элементы, типы
+- [skd-dsl-spec.md](skd-dsl-spec.md) — JSON DSL для описания СКД (формат входных данных `/skd-compile`)