From 8c833f042c76589e0d3b6bf683637cffacdfd860 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 17:24:43 +0300 Subject: [PATCH 01/35] Add /skd-info skill for DCS (Data Composition Schema) analysis Implements 5 modes: overview (compact TOC), query (raw SQL with batch splitting), fields (field table with roles/restrictions), params (parameter table with types/defaults), variant (structure tree with filters and output settings). Update DCS spec with totalField group info. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-info/SKILL.md | 182 ++++ .claude/skills/skd-info/scripts/skd-info.ps1 | 961 +++++++++++++++++++ docs/1c-dcs-spec.md | 23 +- 3 files changed, 1165 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/skd-info/SKILL.md create mode 100644 .claude/skills/skd-info/scripts/skd-info.ps1 diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md new file mode 100644 index 00000000..4cb4a713 --- /dev/null +++ b/.claude/skills/skd-info/SKILL.md @@ -0,0 +1,182 @@ +--- +name: skd-info +description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты +argument-hint: [-Mode overview|query|fields|params|variant] [-Name ] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /skd-info — Анализ схемы компоновки данных + +Читает Template.xml схемы компоновки данных (СКД) и выводит компактную сводку. Заменяет необходимость читать тысячи строк XML. + +## Использование + +``` +/skd-info +/skd-info -Mode query -Name НаборДанных1 +``` + +## Параметры + +| Параметр | Обязательный | По умолчанию | Описание | +|--------------|:------------:|--------------|---------------------------------------------------| +| TemplatePath | да | — | Путь к Template.xml или каталогу макета | +| Mode | нет | `overview` | Режим: `overview`, `query`, `fields`, `params`, `variant` | +| Name | нет | — | Имя набора (query/fields) или варианта (variant) | +| Batch | нет | `0` | Номер пакета запроса (0 = все). Только для query | +| Limit | нет | `150` | Макс. строк вывода (защита от переполнения) | +| Offset | нет | `0` | Пропустить N строк (для пагинации) | +| 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 +... -Mode fields -Name НаборДанных1 +... -Mode params +... -Mode variant -Name Основной +... -Mode variant -Name 1 +``` + +Для большого вывода используй `-OutFile`: +```powershell +... -Mode query -Name ДанныеТ13 -OutFile test-tmp\query.txt +``` +Затем прочитай результат через Read tool. + +## Режимы + +### overview (по умолчанию) — оглавление + +Всегда 20-50 строк. Показывает "карту" схемы для выбора дальнейших действий: + +``` +=== DCS: Template.xml (361 lines) === + +Sources: ИсточникДанных1 (Local) + +Datasets: + [Query] НоменклатураСЦенами 7 fields, query 40 lines +Links: (none) +Calculated: УИД +Totals: Цена=Максимум(Цена) +Templates: Макет1(ТипЦен/Header) +Params: (none) + +Variants: + [1] НоменклатураИЦены "Номенклатура и цены" Table 3 filters + [2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters +``` + +Для DataSetUnion — дерево: +``` +Datasets: + [Union] РасчетНалога 35 fields + ├─ [Query] ДанныеПоСреднегодовой query 120 lines + └─ [Query] ДанныеПоКадастровой query 85 lines + [Object] ДопДанные objectName=Таблица 4 fields +``` + +### query — текст запроса + +Извлекает raw-текст запроса с деэкранированием XML (`&`→`&`, `>`→`>`). Для пакетных запросов — оглавление батчей: + +``` +=== Query: ДанныеТ13 (334 lines, 10 batches) === + Batch 1: lines 1-8 → ПОМЕСТИТЬ Представления_Периоды + Batch 2: lines 9-26 → ПОМЕСТИТЬ Представления_СотрудникиОрганизации + ... +--- Batch 1 --- +ВЫБРАТЬ + ДАТАВРЕМЯ(1, 1, 1) КАК Период +ПОМЕСТИТЬ Представления_Периоды +... +``` + +Фильтр по номеру батча: `-Batch 3` покажет только 3-й пакет. + +### fields — таблица полей + +``` +=== Fields: НоменклатураСЦенами (7) === + dataPath title role restrict format + Номенклатура - - - - + Номенклатура.Артикул "Артикул" - - - + Цена - - - ЧГ=0 +--- calculated --- + УИД = БухгалтерскиеОтчеты.ПолучитьУИДСсылкиСтрокой(Номенклатура) restrict:cond,grp,ord +--- totals --- + Цена = Максимум(Цена) + ПравоИнтерактивное = Максимум(ПравоИнтерактивное) [group:ОбъектМетаданных] +``` + +### params — параметры схемы + +``` +=== Parameters (16) === + Name Type Default Visible Expression + Период StandardPeriod LastMonth yes - + НачалоПериода DateTime - hidden &Период.ДатаНачала + Организация CatalogRef.Организации null yes - +``` + +### variant — структура варианта + +``` +=== 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 +``` + +## Разрешение пути + +- Прямой путь: `path/to/Template.xml` +- Каталог макета: `path/to/ИмяМакета/` → авто-резолв в `Ext/Template.xml` + +## Что не выводится + +- XML namespace-декларации +- Обёртки v8:item/v8:lang/v8:content (извлекаем чистый текст) +- userSettingID (GUID-ы пользовательских настроек) +- Дефолтные periodAdditionBegin/End = 0001-01-01 +- viewMode + +## Когда использовать + +- **Перед анализом отчёта**: overview для понимания структуры +- **Отладка данных**: query для просмотра текста запроса +- **Модификация полей**: fields для полного списка с ролями +- **Программный вызов**: params для списка параметров +- **Изменение вывода**: variant для структуры группировок и фильтров + +## Защита от переполнения + +Вывод ограничен 150 строками по умолчанию. При превышении: +``` +[TRUNCATED] Shown 150 of 400 lines. Use -Offset 150 to continue. +``` + +Используйте `-Offset N` и `-Limit N` для постраничного просмотра. 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..39a6a0e6 --- /dev/null +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -0,0 +1,961 @@ +param( + [Parameter(Mandatory=$true)] + [string]$TemplatePath, + [ValidateSet("overview", "query", "fields", "params", "variant")] + [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") + +$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 + $links = $root.SelectNodes("s:dataSetLink", $ns) + if ($links.Count -gt 0) { + $linkStrs = @() + 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) + $paramStr = if ($paramNode) { " param=$($paramNode.InnerText)" } else { "" } + $linkStrs += "$srcDs.$srcExpr -> $dstDs.$dstExpr$paramStr" + } + $lines.Add("Links: " + ($linkStrs -join "; ")) + } else { + $lines.Add("Links: (none)") + } + + # Calculated fields + $calcFields = $root.SelectNodes("s:calculatedField", $ns) + if ($calcFields.Count -gt 0) { + $calcNames = @() + foreach ($cf in $calcFields) { + $calcNames += $cf.SelectSingleNode("s:dataPath", $ns).InnerText + } + $lines.Add("Calculated: " + ($calcNames -join ", ")) + } + + # Totals + $totalFields = $root.SelectNodes("s:totalField", $ns) + if ($totalFields.Count -gt 0) { + $totalStrs = @() + foreach ($tf in $totalFields) { + $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText + $tfExpr = $tf.SelectSingleNode("s:expression", $ns).InnerText + $tfGroup = $tf.SelectSingleNode("s:group", $ns) + $groupStr = "" + if ($tfGroup) { $groupStr = " [group:$($tfGroup.InnerText)]" } + $totalStrs += "$tfPath=$tfExpr$groupStr" + } + $lines.Add("Totals: " + ($totalStrs -join ", ")) + } + + # Templates + $templates = $root.SelectNodes("s:template", $ns) + $groupTemplates = $root.SelectNodes("s:groupTemplate", $ns) + if ($templates.Count -gt 0 -or $groupTemplates.Count -gt 0) { + $tplStrs = @() + foreach ($tpl in $templates) { + $tplName = $tpl.SelectSingleNode("s:name", $ns).InnerText + $tplStrs += $tplName + } + foreach ($gt in $groupTemplates) { + $gtField = $gt.SelectSingleNode("s:groupField", $ns).InnerText + $gtType = $gt.SelectSingleNode("s:templateType", $ns).InnerText + $gtTpl = $gt.SelectSingleNode("s:template", $ns).InnerText + $tplStrs += "$gtTpl($gtField/$gtType)" + } + $lines.Add("Templates: " + ($tplStrs -join ", ")) + } + + # Parameters + $params = $root.SelectNodes("s:parameter", $ns) + if ($params.Count -gt 0) { + $paramStrs = @() + foreach ($p in $params) { + $pName = $p.SelectSingleNode("s:name", $ns).InnerText + $paramStrs += $pName + } + $lines.Add("Params ($($params.Count)): " + ($paramStrs -join ", ")) + } 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" + } + } + $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") + } + } +} + +# ============================================================ +# MODE: query +# ============================================================ +elseif ($Mode -eq "query") { + + # Find dataset + $dataSets = $root.SelectNodes("s:dataSet", $ns) + $targetDs = $null + + if ($Name) { + # Search by name in top-level and nested + foreach ($ds in $dataSets) { + $dsNameNode = $ds.SelectSingleNode("s:name", $ns) + if ($dsNameNode -and $dsNameNode.InnerText -eq $Name) { $targetDs = $ds; break } + # Search in Union items + 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 } + } + 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) { + 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) { + $found = $false + foreach ($ds in $dataSets) { + $dsNameNode = $ds.SelectSingleNode("s:name", $ns) + if ($dsNameNode -and $dsNameNode.InnerText -eq $Name) { + Show-DataSetFields $ds + $found = $true + break + } + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + $subNameNode = $subDs.SelectSingleNode("s:name", $ns) + if ($subNameNode -and $subNameNode.InnerText -eq $Name) { + Show-DataSetFields $subDs + $found = $true + break + } + } + if ($found) { break } + } + if (-not $found) { + Write-Error "Dataset '$Name' not found" + exit 1 + } + } else { + # Show all datasets + $first = $true + foreach ($ds in $dataSets) { + if (-not $first) { $lines.Add("") } + $first = $false + Show-DataSetFields $ds + + $dsType = Get-DataSetType $ds + if ($dsType -eq "Union") { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + $lines.Add("") + Show-DataSetFields $subDs + } + } + } + } + + # Calculated fields + $calcFields = $root.SelectNodes("s:calculatedField", $ns) + if ($calcFields.Count -gt 0) { + $lines.Add("") + $lines.Add("--- calculated ---") + foreach ($cf in $calcFields) { + $cfPath = $cf.SelectSingleNode("s:dataPath", $ns).InnerText + $cfExpr = $cf.SelectSingleNode("s:expression", $ns).InnerText + $cfRestrict = $cf.SelectSingleNode("s:useRestriction", $ns) + $restrictStr = "" + if ($cfRestrict) { + $parts = @() + foreach ($child in $cfRestrict.ChildNodes) { + if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { + $parts += $child.LocalName.Substring(0, [Math]::Min(4, $child.LocalName.Length)) + } + } + if ($parts.Count -gt 0) { $restrictStr = " restrict:" + ($parts -join ",") } + } + $lines.Add(" $cfPath = $cfExpr$restrictStr") + } + } + + # Total fields + $totalFields = $root.SelectNodes("s:totalField", $ns) + if ($totalFields.Count -gt 0) { + $lines.Add("") + $lines.Add("--- totals ---") + foreach ($tf in $totalFields) { + $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText + $tfExpr = $tf.SelectSingleNode("s:expression", $ns).InnerText + $tfGroup = $tf.SelectSingleNode("s:group", $ns) + $groupStr = "" + if ($tfGroup) { $groupStr = " [group:$($tfGroup.InnerText)]" } + $lines.Add(" $tfPath = $tfExpr$groupStr") + } + } +} + +# ============================================================ +# 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) + + $targetVariant = $null + if ($Name) { + # Try by name + $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" + exit 1 + } + } else { + # Take first + $targetVariant = $variants[0] + $matchIdx = 1 + if (-not $targetVariant) { + Write-Error "No variants found" + 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 " ")) + } + } + } +} + +# --- 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/docs/1c-dcs-spec.md b/docs/1c-dcs-spec.md index 6c07c0a3..c97b7242 100644 --- a/docs/1c-dcs-spec.md +++ b/docs/1c-dcs-spec.md @@ -446,7 +446,28 @@ DataCompositionSchema |---|---|---| | `dataPath` | да | Путь к полю | | `expression` | да | Агрегатная функция: `Сумма(...)`, `Количество(...)`, `Максимум(...)`, `Минимум(...)`, `Среднее(...)` | -| `group` | нет | Для какой группировки считать итоги | +| `group` | нет | Имя группировки, для которой считать итоги. Без `group` — для всех группировок | + +### Разные формулы для разных группировок + +Одно поле может иметь несколько записей `totalField` с разными формулами для разных группировок: + +```xml + + + ПравоИнтерактивное + Максимум(ПравоИнтерактивное) + ОбъектМетаданных + + + + ПравоИнтерактивное + Максимум(ПравоОтчета) + Отчет + +``` + +Это позволяет вычислять ресурс по-разному в зависимости от контекста группировки. --- From 9922f118d1de8708655af9886f848ba26be9c10a Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 17:31:30 +0300 Subject: [PATCH 02/35] Improve /skd-info overview for large schemas Switch to multi-line/compact format when element counts are high: totals (>5) show grouped by field name, calculated (>10) truncated, templates (>10) show counts, links (>2) multi-line, variant structure groups identical items (e.g. "17x Group(...)"). Fix query mode to prefer nested Query datasets over parent Union with same name. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-info/scripts/skd-info.ps1 | 110 +++++++++++++++---- 1 file changed, 88 insertions(+), 22 deletions(-) diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index 39a6a0e6..e467d9e9 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -360,7 +360,12 @@ if ($Mode -eq "overview") { $paramStr = if ($paramNode) { " param=$($paramNode.InnerText)" } else { "" } $linkStrs += "$srcDs.$srcExpr -> $dstDs.$dstExpr$paramStr" } - $lines.Add("Links: " + ($linkStrs -join "; ")) + if ($linkStrs.Count -le 2) { + $lines.Add("Links: " + ($linkStrs -join "; ")) + } else { + $lines.Add("Links ($($linkStrs.Count)):") + foreach ($ls in $linkStrs) { $lines.Add(" $ls") } + } } else { $lines.Add("Links: (none)") } @@ -372,40 +377,71 @@ if ($Mode -eq "overview") { foreach ($cf in $calcFields) { $calcNames += $cf.SelectSingleNode("s:dataPath", $ns).InnerText } - $lines.Add("Calculated: " + ($calcNames -join ", ")) + if ($calcNames.Count -le 10) { + $lines.Add("Calculated: " + ($calcNames -join ", ")) + } else { + $lines.Add("Calculated ($($calcNames.Count)): " + (($calcNames[0..9] -join ", ")) + ", ...") + } } # Totals $totalFields = $root.SelectNodes("s:totalField", $ns) if ($totalFields.Count -gt 0) { - $totalStrs = @() - foreach ($tf in $totalFields) { - $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText - $tfExpr = $tf.SelectSingleNode("s:expression", $ns).InnerText - $tfGroup = $tf.SelectSingleNode("s:group", $ns) - $groupStr = "" - if ($tfGroup) { $groupStr = " [group:$($tfGroup.InnerText)]" } - $totalStrs += "$tfPath=$tfExpr$groupStr" + if ($totalFields.Count -le 5) { + $totalStrs = @() + foreach ($tf in $totalFields) { + $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText + $tfExpr = $tf.SelectSingleNode("s:expression", $ns).InnerText + $tfGroup = $tf.SelectSingleNode("s:group", $ns) + $groupStr = "" + if ($tfGroup) { $groupStr = " [group:$($tfGroup.InnerText)]" } + $totalStrs += "$tfPath=$tfExpr$groupStr" + } + $lines.Add("Totals: " + ($totalStrs -join ", ")) + } else { + # Compact: group by dataPath, show unique paths with count + $pathCounts = [ordered]@{} + $hasGrouped = $false + foreach ($tf in $totalFields) { + $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText + $tfGroup = $tf.SelectSingleNode("s:group", $ns) + if ($tfGroup) { $hasGrouped = $true } + if (-not $pathCounts.Contains($tfPath)) { $pathCounts[$tfPath] = 0 } + $pathCounts[$tfPath] = $pathCounts[$tfPath] + 1 + } + $uniquePaths = @($pathCounts.Keys) + $groupNote = if ($hasGrouped) { ", some with group-specific formulas" } else { "" } + if ($uniquePaths.Count -le 10) { + $lines.Add("Totals ($($totalFields.Count) for $($uniquePaths.Count) fields$groupNote): " + ($uniquePaths -join ", ")) + } else { + $lines.Add("Totals ($($totalFields.Count) for $($uniquePaths.Count) fields$groupNote):") + $lines.Add(" " + (($uniquePaths[0..14] -join ", ")) + ", ...") + } } - $lines.Add("Totals: " + ($totalStrs -join ", ")) } # Templates $templates = $root.SelectNodes("s:template", $ns) $groupTemplates = $root.SelectNodes("s:groupTemplate", $ns) if ($templates.Count -gt 0 -or $groupTemplates.Count -gt 0) { - $tplStrs = @() + $tplNames = @() foreach ($tpl in $templates) { - $tplName = $tpl.SelectSingleNode("s:name", $ns).InnerText - $tplStrs += $tplName + $tplNames += $tpl.SelectSingleNode("s:name", $ns).InnerText } + $gtStrs = @() foreach ($gt in $groupTemplates) { $gtField = $gt.SelectSingleNode("s:groupField", $ns).InnerText $gtType = $gt.SelectSingleNode("s:templateType", $ns).InnerText $gtTpl = $gt.SelectSingleNode("s:template", $ns).InnerText - $tplStrs += "$gtTpl($gtField/$gtType)" + $gtStrs += "$gtTpl($gtField/$gtType)" + } + $totalTpl = $tplNames.Count + $gtStrs.Count + if ($totalTpl -le 10) { + $all = $tplNames + $gtStrs + $lines.Add("Templates: " + ($all -join ", ")) + } else { + $lines.Add("Templates: $($tplNames.Count) templates, $($groupTemplates.Count) group bindings") } - $lines.Add("Templates: " + ($tplStrs -join ", ")) } # Parameters @@ -416,7 +452,11 @@ if ($Mode -eq "overview") { $pName = $p.SelectSingleNode("s:name", $ns).InnerText $paramStrs += $pName } - $lines.Add("Params ($($params.Count)): " + ($paramStrs -join ", ")) + if ($params.Count -le 15) { + $lines.Add("Params ($($params.Count)): " + ($paramStrs -join ", ")) + } else { + $lines.Add("Params ($($params.Count)): " + (($paramStrs[0..9] -join ", ")) + ", ...") + } } else { $lines.Add("Params: (none)") } @@ -448,6 +488,16 @@ if ($Mode -eq "overview") { $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 @@ -471,17 +521,22 @@ elseif ($Mode -eq "query") { $targetDs = $null if ($Name) { - # Search by name in top-level and nested + # Search by name: prefer nested Query items over parent Union + # Pass 1: search nested items first foreach ($ds in $dataSets) { - $dsNameNode = $ds.SelectSingleNode("s:name", $ns) - if ($dsNameNode -and $dsNameNode.InnerText -eq $Name) { $targetDs = $ds; break } - # Search in Union items 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 @@ -506,7 +561,18 @@ elseif ($Mode -eq "query") { $queryNode = $targetDs.SelectSingleNode("s:query", $ns) if (-not $queryNode) { - Write-Error "Dataset has no query element" + # 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 } From e86620dbd754200b22d8e8bc0e933a8d1a5864d7 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 17:39:16 +0300 Subject: [PATCH 03/35] Simplify /skd-info overview to pure navigation map Overview now shows only counts for calculated/totals/templates. Links compressed to dataset pairs. Params split into visible/hidden with only visible names listed. Add "Next:" hints with available modes and dataset names to guide further exploration. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-info/scripts/skd-info.ps1 | 160 +++++++++---------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index e467d9e9..f666d847 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -347,116 +347,81 @@ if ($Mode -eq "overview") { } } - # Links + # Links — only dataset pairs (not field-level) $links = $root.SelectNodes("s:dataSetLink", $ns) if ($links.Count -gt 0) { - $linkStrs = @() + $linkPairs = [ordered]@{} 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) - $paramStr = if ($paramNode) { " param=$($paramNode.InnerText)" } else { "" } - $linkStrs += "$srcDs.$srcExpr -> $dstDs.$dstExpr$paramStr" + $key = "$srcDs -> $dstDs" + if (-not $linkPairs.Contains($key)) { $linkPairs[$key] = 0 } + $linkPairs[$key] = $linkPairs[$key] + 1 } - if ($linkStrs.Count -le 2) { - $lines.Add("Links: " + ($linkStrs -join "; ")) - } else { - $lines.Add("Links ($($linkStrs.Count)):") - foreach ($ls in $linkStrs) { $lines.Add(" $ls") } + $linkStrs = @() + foreach ($key in $linkPairs.Keys) { + $cnt = $linkPairs[$key] + if ($cnt -gt 1) { $linkStrs += "$key (${cnt} fields)" } + else { $linkStrs += $key } } - } else { - $lines.Add("Links: (none)") + $lines.Add("Links: " + ($linkStrs -join ", ")) } - # Calculated fields + # Calculated fields — count only $calcFields = $root.SelectNodes("s:calculatedField", $ns) if ($calcFields.Count -gt 0) { - $calcNames = @() - foreach ($cf in $calcFields) { - $calcNames += $cf.SelectSingleNode("s:dataPath", $ns).InnerText - } - if ($calcNames.Count -le 10) { - $lines.Add("Calculated: " + ($calcNames -join ", ")) - } else { - $lines.Add("Calculated ($($calcNames.Count)): " + (($calcNames[0..9] -join ", ")) + ", ...") - } + $lines.Add("Calculated: $($calcFields.Count)") } - # Totals + # Totals — count + group flag $totalFields = $root.SelectNodes("s:totalField", $ns) if ($totalFields.Count -gt 0) { - if ($totalFields.Count -le 5) { - $totalStrs = @() - foreach ($tf in $totalFields) { - $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText - $tfExpr = $tf.SelectSingleNode("s:expression", $ns).InnerText - $tfGroup = $tf.SelectSingleNode("s:group", $ns) - $groupStr = "" - if ($tfGroup) { $groupStr = " [group:$($tfGroup.InnerText)]" } - $totalStrs += "$tfPath=$tfExpr$groupStr" - } - $lines.Add("Totals: " + ($totalStrs -join ", ")) + $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("Totals: $($totalFields.Count)$groupNote") } else { - # Compact: group by dataPath, show unique paths with count - $pathCounts = [ordered]@{} - $hasGrouped = $false - foreach ($tf in $totalFields) { - $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText - $tfGroup = $tf.SelectSingleNode("s:group", $ns) - if ($tfGroup) { $hasGrouped = $true } - if (-not $pathCounts.Contains($tfPath)) { $pathCounts[$tfPath] = 0 } - $pathCounts[$tfPath] = $pathCounts[$tfPath] + 1 - } - $uniquePaths = @($pathCounts.Keys) - $groupNote = if ($hasGrouped) { ", some with group-specific formulas" } else { "" } - if ($uniquePaths.Count -le 10) { - $lines.Add("Totals ($($totalFields.Count) for $($uniquePaths.Count) fields$groupNote): " + ($uniquePaths -join ", ")) - } else { - $lines.Add("Totals ($($totalFields.Count) for $($uniquePaths.Count) fields$groupNote):") - $lines.Add(" " + (($uniquePaths[0..14] -join ", ")) + ", ...") - } + $lines.Add("Totals: $($totalFields.Count) ($($uniquePaths.Count) fields$groupNote)") } } - # Templates + # Templates — count only $templates = $root.SelectNodes("s:template", $ns) $groupTemplates = $root.SelectNodes("s:groupTemplate", $ns) if ($templates.Count -gt 0 -or $groupTemplates.Count -gt 0) { - $tplNames = @() - foreach ($tpl in $templates) { - $tplNames += $tpl.SelectSingleNode("s:name", $ns).InnerText - } - $gtStrs = @() - foreach ($gt in $groupTemplates) { - $gtField = $gt.SelectSingleNode("s:groupField", $ns).InnerText - $gtType = $gt.SelectSingleNode("s:templateType", $ns).InnerText - $gtTpl = $gt.SelectSingleNode("s:template", $ns).InnerText - $gtStrs += "$gtTpl($gtField/$gtType)" - } - $totalTpl = $tplNames.Count + $gtStrs.Count - if ($totalTpl -le 10) { - $all = $tplNames + $gtStrs - $lines.Add("Templates: " + ($all -join ", ")) - } else { - $lines.Add("Templates: $($tplNames.Count) templates, $($groupTemplates.Count) group bindings") - } + $parts = @() + if ($templates.Count -gt 0) { $parts += "$($templates.Count) templates" } + if ($groupTemplates.Count -gt 0) { $parts += "$($groupTemplates.Count) group bindings" } + $lines.Add("Templates: " + ($parts -join ", ")) } - # Parameters + # Parameters — split visible/hidden $params = $root.SelectNodes("s:parameter", $ns) if ($params.Count -gt 0) { - $paramStrs = @() + $visibleNames = @() + $hiddenCount = 0 foreach ($p in $params) { $pName = $p.SelectSingleNode("s:name", $ns).InnerText - $paramStrs += $pName + $useRestrict = $p.SelectSingleNode("s:useRestriction", $ns) + $isHidden = ($useRestrict -and $useRestrict.InnerText -eq "true") + if ($isHidden) { $hiddenCount++ } else { $visibleNames += $pName } } - if ($params.Count -le 15) { - $lines.Add("Params ($($params.Count)): " + ($paramStrs -join ", ")) - } else { - $lines.Add("Params ($($params.Count)): " + (($paramStrs[0..9] -join ", ")) + ", ...") + $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)") } @@ -509,6 +474,41 @@ if ($Mode -eq "overview") { $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 details + calculated + totals" + 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))" + } + $lines.Add("Next:") + foreach ($h in $hints) { $lines.Add(" $h") } } # ============================================================ From 2949682ce8e2a0dd641aa7442bd7ffc2c744b261 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 17:39:42 +0300 Subject: [PATCH 04/35] Update SKILL.md examples to match simplified overview Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-info/SKILL.md | 36 ++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md index 4cb4a713..6d39d0f8 100644 --- a/.claude/skills/skd-info/SKILL.md +++ b/.claude/skills/skd-info/SKILL.md @@ -56,35 +56,45 @@ powershell.exe -NoProfile -File .claude\skills\skd-info\scripts\skd-info.ps1 -Te ## Режимы -### overview (по умолчанию) — оглавление +### overview (по умолчанию) — карта схемы -Всегда 20-50 строк. Показывает "карту" схемы для выбора дальнейших действий: +Компактная навигационная карта (10-25 строк). Показывает структуру и подсказывает следующие шаги: ``` -=== DCS: Template.xml (361 lines) === +=== DCS: ОсновнаяСхемаКомпоновкиДанных (362 lines) === Sources: ИсточникДанных1 (Local) Datasets: [Query] НоменклатураСЦенами 7 fields, query 40 lines -Links: (none) -Calculated: УИД -Totals: Цена=Максимум(Цена) -Templates: Макет1(ТипЦен/Header) +Calculated: 1 +Totals: 1 +Templates: 1 templates, 1 group bindings Params: (none) Variants: - [1] НоменклатураИЦены "Номенклатура и цены" Table 3 filters + [1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters [2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters + +Next: + -Mode query query text + -Mode fields field details + calculated + totals + -Mode variant -Name variant structure (1..2) ``` -Для DataSetUnion — дерево: +Для DataSetUnion — дерево наборов + связи: ``` Datasets: - [Union] РасчетНалога 35 fields - ├─ [Query] ДанныеПоСреднегодовой query 120 lines - └─ [Query] ДанныеПоКадастровой query 85 lines - [Object] ДопДанные objectName=Таблица 4 fields + [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 — текст запроса From d210d21079ebef9296f334212b61eb3485142b4e Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 17:46:29 +0300 Subject: [PATCH 05/35] Add full link details to fields mode Show dataSetLink field-level mappings (source.field -> dest.field param=X) at the top of -Mode fields output. Overview keeps compact dataset-pair summary, fields provides the detail. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-info/scripts/skd-info.ps1 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index f666d847..85b08791 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -646,6 +646,22 @@ elseif ($Mode -eq "query") { # ============================================================ elseif ($Mode -eq "fields") { + # Links (full detail) + $links = $root.SelectNodes("s:dataSetLink", $ns) + if ($links.Count -gt 0) { + $lines.Add("--- links ---") + 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) + $paramStr = if ($paramNode) { " param=$($paramNode.InnerText)" } else { "" } + $lines.Add(" $srcDs.$srcExpr -> $dstDs.$dstExpr$paramStr") + } + $lines.Add("") + } + $dataSets = $root.SelectNodes("s:dataSet", $ns) function Show-DataSetFields($dsNode) { From 24358f212f01ad94851edce50c5ce8b3c9c56946 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 18:00:30 +0300 Subject: [PATCH 06/35] Split fields mode into fields, links, and totals modes Separate concerns for cleaner output: fields shows only dataset field tables, links shows dataset connections, totals shows calculated fields and resources. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-info/SKILL.md | 41 +++++-- .claude/skills/skd-info/scripts/skd-info.ps1 | 110 ++++++++++++++----- 2 files changed, 113 insertions(+), 38 deletions(-) diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md index 6d39d0f8..525420ba 100644 --- a/.claude/skills/skd-info/SKILL.md +++ b/.claude/skills/skd-info/SKILL.md @@ -1,7 +1,7 @@ --- name: skd-info description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты -argument-hint: [-Mode overview|query|fields|params|variant] [-Name ] +argument-hint: [-Mode overview|query|fields|links|totals|params|variant] [-Name ] allowed-tools: - Bash - Read @@ -24,7 +24,7 @@ allowed-tools: | Параметр | Обязательный | По умолчанию | Описание | |--------------|:------------:|--------------|---------------------------------------------------| | TemplatePath | да | — | Путь к Template.xml или каталогу макета | -| Mode | нет | `overview` | Режим: `overview`, `query`, `fields`, `params`, `variant` | +| Mode | нет | `overview` | Режим: `overview`, `query`, `fields`, `links`, `totals`, `params`, `variant` | | Name | нет | — | Имя набора (query/fields) или варианта (variant) | | Batch | нет | `0` | Номер пакета запроса (0 = все). Только для query | | Limit | нет | `150` | Макс. строк вывода (защита от переполнения) | @@ -43,6 +43,8 @@ powershell.exe -NoProfile -File .claude\skills\skd-info\scripts\skd-info.ps1 -Te ... -Mode query -Name ДанныеТ13 -Batch 3 ... -Mode fields ... -Mode fields -Name НаборДанных1 +... -Mode links +... -Mode totals ... -Mode params ... -Mode variant -Name Основной ... -Mode variant -Name 1 @@ -78,7 +80,8 @@ Variants: Next: -Mode query query text - -Mode fields field details + calculated + totals + -Mode fields field tables by dataset + -Mode totals calculated fields + resources -Mode variant -Name variant structure (1..2) ``` @@ -115,17 +118,37 @@ Params: 18 (7 visible, 11 hidden): Период, Ответственный, ... Фильтр по номеру батча: `-Batch 3` покажет только 3-й пакет. -### fields — таблица полей +### fields — таблица полей наборов данных ``` -=== Fields: НоменклатураСЦенами (7) === +=== Fields: НоменклатураСЦенами [Query] (7) === dataPath title role restrict format Номенклатура - - - - Номенклатура.Артикул "Артикул" - - - Цена - - - ЧГ=0 ---- calculated --- +``` + +Без `-Name` — поля всех наборов. С `-Name <набор>` — только указанного. + +### links — связи наборов данных + +``` +=== Links (5) === + +РасчетНалогаНаИмущество -> СостояниеОС : + Организация -> Организация + ОсновноеСредство -> ОсновноеСредство +``` + +Группирует по парам наборов. Показывает поля связи и параметры. + +### totals — вычисляемые поля и ресурсы + +``` +=== Calculated fields (3) === УИД = БухгалтерскиеОтчеты.ПолучитьУИДСсылкиСтрокой(Номенклатура) restrict:cond,grp,ord ---- totals --- + +=== Resources (2) === Цена = Максимум(Цена) ПравоИнтерактивное = Максимум(ПравоИнтерактивное) [group:ОбъектМетаданных] ``` @@ -178,7 +201,9 @@ Output: style=ЧерноБелый groups=Separately totalsH=None totalsV=Non - **Перед анализом отчёта**: overview для понимания структуры - **Отладка данных**: query для просмотра текста запроса -- **Модификация полей**: fields для полного списка с ролями +- **Модификация полей**: fields для списка с ролями по наборам +- **Связи между наборами**: links для полей связи и параметров +- **Формулы и ресурсы**: totals для вычисляемых полей и ресурсов - **Программный вызов**: params для списка параметров - **Изменение вывода**: variant для структуры группировок и фильтров diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index 85b08791..1836755f 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -1,7 +1,7 @@ param( [Parameter(Mandatory=$true)] [string]$TemplatePath, - [ValidateSet("overview", "query", "fields", "params", "variant")] + [ValidateSet("overview", "query", "fields", "links", "totals", "params", "variant")] [string]$Mode = "overview", [string]$Name, [int]$Batch = 0, @@ -498,7 +498,16 @@ if ($Mode -eq "overview") { } elseif ($queryDsNames.Count -gt 1) { $hints += "-Mode query -Name query text ($($queryDsNames -join ', '))" } - $hints += "-Mode fields field details + calculated + totals" + $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 -or $totalCount -gt 0) { + $hints += "-Mode totals calculated fields + resources" + } if ($params.Count -gt 0) { $hints += "-Mode params parameter details" } @@ -646,22 +655,6 @@ elseif ($Mode -eq "query") { # ============================================================ elseif ($Mode -eq "fields") { - # Links (full detail) - $links = $root.SelectNodes("s:dataSetLink", $ns) - if ($links.Count -gt 0) { - $lines.Add("--- links ---") - 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) - $paramStr = if ($paramNode) { " param=$($paramNode.InnerText)" } else { "" } - $lines.Add(" $srcDs.$srcExpr -> $dstDs.$dstExpr$paramStr") - } - $lines.Add("") - } - $dataSets = $root.SelectNodes("s:dataSet", $ns) function Show-DataSetFields($dsNode) { @@ -734,14 +727,9 @@ elseif ($Mode -eq "fields") { } if ($Name) { + # Search nested items first (Union child over Union parent) $found = $false foreach ($ds in $dataSets) { - $dsNameNode = $ds.SelectSingleNode("s:name", $ns) - if ($dsNameNode -and $dsNameNode.InnerText -eq $Name) { - Show-DataSetFields $ds - $found = $true - break - } foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { $subNameNode = $subDs.SelectSingleNode("s:name", $ns) if ($subNameNode -and $subNameNode.InnerText -eq $Name) { @@ -752,6 +740,16 @@ elseif ($Mode -eq "fields") { } if ($found) { break } } + if (-not $found) { + foreach ($ds in $dataSets) { + $dsNameNode = $ds.SelectSingleNode("s:name", $ns) + if ($dsNameNode -and $dsNameNode.InnerText -eq $Name) { + Show-DataSetFields $ds + $found = $true + break + } + } + } if (-not $found) { Write-Error "Dataset '$Name' not found" exit 1 @@ -773,15 +771,63 @@ elseif ($Mode -eq "fields") { } } } +} + +# ============================================================ +# 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: totals +# ============================================================ +elseif ($Mode -eq "totals") { # Calculated fields $calcFields = $root.SelectNodes("s:calculatedField", $ns) if ($calcFields.Count -gt 0) { - $lines.Add("") - $lines.Add("--- calculated ---") + $lines.Add("=== Calculated fields ($($calcFields.Count)) ===") foreach ($cf in $calcFields) { $cfPath = $cf.SelectSingleNode("s:dataPath", $ns).InnerText $cfExpr = $cf.SelectSingleNode("s:expression", $ns).InnerText + $cfTitle = $cf.SelectSingleNode("s:title", $ns) + $titleStr = "" + if ($cfTitle) { + $t = Get-MLText $cfTitle + if ($t) { $titleStr = " `"$t`"" } + } + $cfRestrict = $cf.SelectSingleNode("s:useRestriction", $ns) $restrictStr = "" if ($cfRestrict) { @@ -793,15 +839,15 @@ elseif ($Mode -eq "fields") { } if ($parts.Count -gt 0) { $restrictStr = " restrict:" + ($parts -join ",") } } - $lines.Add(" $cfPath = $cfExpr$restrictStr") + $lines.Add(" $cfPath = $cfExpr$restrictStr$titleStr") } + $lines.Add("") } - # Total fields + # Total fields (resources) $totalFields = $root.SelectNodes("s:totalField", $ns) if ($totalFields.Count -gt 0) { - $lines.Add("") - $lines.Add("--- totals ---") + $lines.Add("=== Resources ($($totalFields.Count)) ===") foreach ($tf in $totalFields) { $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText $tfExpr = $tf.SelectSingleNode("s:expression", $ns).InnerText @@ -811,6 +857,10 @@ elseif ($Mode -eq "fields") { $lines.Add(" $tfPath = $tfExpr$groupStr") } } + + if ($calcFields.Count -eq 0 -and $totalFields.Count -eq 0) { + $lines.Add("(no calculated fields or resources)") + } } # ============================================================ From 941fa73803d5737e52030ed9bf7b8cba44e6be10 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 18:23:02 +0300 Subject: [PATCH 07/35] Add progressive disclosure to fields and totals modes Without -Name, both modes now show a compact map (field names per dataset / calculated+resource names). With -Name, they drill down to full detail. Totals -Name shows both calculated expression and resource formula when field appears in both. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-info/SKILL.md | 52 ++++-- .claude/skills/skd-info/scripts/skd-info.ps1 | 165 ++++++++++++++----- 2 files changed, 160 insertions(+), 57 deletions(-) diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md index 525420ba..98a0f39d 100644 --- a/.claude/skills/skd-info/SKILL.md +++ b/.claude/skills/skd-info/SKILL.md @@ -25,7 +25,7 @@ allowed-tools: |--------------|:------------:|--------------|---------------------------------------------------| | TemplatePath | да | — | Путь к Template.xml или каталогу макета | | Mode | нет | `overview` | Режим: `overview`, `query`, `fields`, `links`, `totals`, `params`, `variant` | -| Name | нет | — | Имя набора (query/fields) или варианта (variant) | +| Name | нет | — | Имя набора (query/fields), поля (totals) или варианта (variant) | | Batch | нет | `0` | Номер пакета запроса (0 = все). Только для query | | Limit | нет | `150` | Макс. строк вывода (защита от переполнения) | | Offset | нет | `0` | Пропустить N строк (для пагинации) | @@ -118,18 +118,25 @@ Params: 18 (7 visible, 11 hidden): Период, Ответственный, ... Фильтр по номеру батча: `-Batch 3` покажет только 3-й пакет. -### fields — таблица полей наборов данных +### fields — поля наборов данных +Без `-Name` — карта: имена полей по наборам: ``` -=== Fields: НоменклатураСЦенами [Query] (7) === +=== Fields map === +СостояниеОС [Query] (3): Организация, ОсновноеСредство, ДатаСостояния +РасчетНалогаНаИмущество [Union] (52): ДоляСтоимостиЧислитель, ... + РасчетНалогаНаИмущество [Query] (51): КадастроваяСтоимость, ... +``` + +С `-Name <набор>` — полная таблица: +``` +=== Fields: СостояниеОС [Query] (3) === dataPath title role restrict format - Номенклатура - - - - - Номенклатура.Артикул "Артикул" - - - - Цена - - - ЧГ=0 + Организация - - - - + ОсновноеСредство Объект - - - + ДатаСостояния Дата ввода в эксплуатацию - - ДФ=dd.MM.yyyy ``` -Без `-Name` — поля всех наборов. С `-Name <набор>` — только указанного. - ### links — связи наборов данных ``` @@ -144,13 +151,32 @@ Params: 18 (7 visible, 11 hidden): Период, Ответственный, ... ### totals — вычисляемые поля и ресурсы +Без `-Name` — карта: имена вычисляемых полей и ресурсов: ``` -=== Calculated fields (3) === - УИД = БухгалтерскиеОтчеты.ПолучитьУИДСсылкиСтрокой(Номенклатура) restrict:cond,grp,ord +=== Calculated fields (23) === + ДоляСтоимости "Доля стоимости" + КоэффициентКи "Коэффициент Ки" + ... -=== Resources (2) === - Цена = Максимум(Цена) - ПравоИнтерактивное = Максимум(ПравоИнтерактивное) [group:ОбъектМетаданных] +=== Resources (51) === + НалоговаяБаза + КоэффициентКи * + ... + * = has group-level formulas +``` + +С `-Name <поле>` — полная формула (если поле есть и в calculated, и в resources — покажет обе части): +``` +=== Calculated: КоэффициентКи === + +Expression: + ВЫБОР КОГДА ... ТОГДА 0 ИНАЧЕ ... КОНЕЦ +Title: Коэффициент Ки +Restrict: condition + +=== Resource: КоэффициентКи === + + [ОсновноеСредство] Сумма(КоэффициентКи) ``` ### params — параметры схемы diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index 1836755f..26d855ea 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -755,21 +755,37 @@ elseif ($Mode -eq "fields") { exit 1 } } else { - # Show all datasets - $first = $true - foreach ($ds in $dataSets) { - if (-not $first) { $lines.Add("") } - $first = $false - Show-DataSetFields $ds + # 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)) { - $lines.Add("") - Show-DataSetFields $subDs + Show-DataSetFieldMap $subDs " " } } } + + $lines.Add("") + $lines.Add("Use -Name for full field table.") } } @@ -814,52 +830,113 @@ elseif ($Mode -eq "links") { # ============================================================ elseif ($Mode -eq "totals") { - # Calculated fields $calcFields = $root.SelectNodes("s:calculatedField", $ns) - if ($calcFields.Count -gt 0) { - $lines.Add("=== Calculated fields ($($calcFields.Count)) ===") + $totalFields = $root.SelectNodes("s:totalField", $ns) + + if ($Name) { + # Detail for specific field + $found = $false + + # Search in calculated fields foreach ($cf in $calcFields) { $cfPath = $cf.SelectSingleNode("s:dataPath", $ns).InnerText - $cfExpr = $cf.SelectSingleNode("s:expression", $ns).InnerText - $cfTitle = $cf.SelectSingleNode("s:title", $ns) - $titleStr = "" - if ($cfTitle) { - $t = Get-MLText $cfTitle - if ($t) { $titleStr = " `"$t`"" } - } + if ($cfPath -eq $Name) { + $lines.Add("=== Calculated: $cfPath ===") + $lines.Add("") - $cfRestrict = $cf.SelectSingleNode("s:useRestriction", $ns) - $restrictStr = "" - if ($cfRestrict) { - $parts = @() - foreach ($child in $cfRestrict.ChildNodes) { - if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { - $parts += $child.LocalName.Substring(0, [Math]::Min(4, $child.LocalName.Length)) - } + $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") } } - if ($parts.Count -gt 0) { $restrictStr = " restrict:" + ($parts -join ",") } - } - $lines.Add(" $cfPath = $cfExpr$restrictStr$titleStr") - } - $lines.Add("") - } - # Total fields (resources) - $totalFields = $root.SelectNodes("s:totalField", $ns) - if ($totalFields.Count -gt 0) { - $lines.Add("=== Resources ($($totalFields.Count)) ===") + $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 + } + } + + # Search in resources (also show if already found in calculated — field can be both) + $matchedResources = @() foreach ($tf in $totalFields) { $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText - $tfExpr = $tf.SelectSingleNode("s:expression", $ns).InnerText - $tfGroup = $tf.SelectSingleNode("s:group", $ns) - $groupStr = "" - if ($tfGroup) { $groupStr = " [group:$($tfGroup.InnerText)]" } - $lines.Add(" $tfPath = $tfExpr$groupStr") + if ($tfPath -eq $Name) { $matchedResources += $tf } + } + if ($matchedResources.Count -gt 0) { + if ($found) { $lines.Add("") } + $lines.Add("=== Resource: $Name ===") + $lines.Add("") + foreach ($tf in $matchedResources) { + $tfExpr = $tf.SelectSingleNode("s:expression", $ns).InnerText + $tfGroup = $tf.SelectSingleNode("s:group", $ns) + $groupStr = "(overall)" + if ($tfGroup) { $groupStr = $tfGroup.InnerText } + $lines.Add(" [$groupStr] $tfExpr") + } + $found = $true } - } - if ($calcFields.Count -eq 0 -and $totalFields.Count -eq 0) { - $lines.Add("(no calculated fields or resources)") + if (-not $found) { + Write-Error "Field '$Name' not found in calculated fields or resources" + exit 1 + } + } else { + # Compact map + if ($calcFields.Count -gt 0) { + $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("") + } + + if ($totalFields.Count -gt 0) { + $lines.Add("=== Resources ($($totalFields.Count)) ===") + # Collect unique field names with group info + $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") + } + + if ($calcFields.Count -eq 0 -and $totalFields.Count -eq 0) { + $lines.Add("(no calculated fields or resources)") + } else { + $lines.Add("") + $lines.Add("Use -Name for full expression.") + } } } From 21dded4d1c8053a342dadcf1ec30de71d65677af Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 18:34:12 +0300 Subject: [PATCH 08/35] Add trace mode for field origin analysis New mode traces a field from title/name to its full origin: dataset fields, calculated expression with operands, resource formulas. Searches by dataPath, exact title, or title substring. Collapses 5-7 manual calls into one. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-info/SKILL.md | 29 ++- .claude/skills/skd-info/scripts/skd-info.ps1 | 196 ++++++++++++++++++- 2 files changed, 222 insertions(+), 3 deletions(-) diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md index 98a0f39d..1ab557d4 100644 --- a/.claude/skills/skd-info/SKILL.md +++ b/.claude/skills/skd-info/SKILL.md @@ -1,7 +1,7 @@ --- name: skd-info description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты -argument-hint: [-Mode overview|query|fields|links|totals|params|variant] [-Name ] +argument-hint: [-Mode overview|query|fields|links|totals|params|variant|trace] [-Name ] allowed-tools: - Bash - Read @@ -24,7 +24,7 @@ allowed-tools: | Параметр | Обязательный | По умолчанию | Описание | |--------------|:------------:|--------------|---------------------------------------------------| | TemplatePath | да | — | Путь к Template.xml или каталогу макета | -| Mode | нет | `overview` | Режим: `overview`, `query`, `fields`, `links`, `totals`, `params`, `variant` | +| Mode | нет | `overview` | Режим: `overview`, `query`, `fields`, `links`, `totals`, `params`, `variant`, `trace` | | Name | нет | — | Имя набора (query/fields), поля (totals) или варианта (variant) | | Batch | нет | `0` | Номер пакета запроса (0 = все). Только для query | | Limit | нет | `150` | Макс. строк вывода (защита от переполнения) | @@ -45,7 +45,10 @@ powershell.exe -NoProfile -File .claude\skills\skd-info\scripts\skd-info.ps1 -Te ... -Mode fields -Name НаборДанных1 ... -Mode links ... -Mode totals +... -Mode totals -Name СуммаНалога ... -Mode params +... -Mode trace -Name КоэффициентКи +... -Mode trace -Name "Коэффициент Ки" ... -Mode variant -Name Основной ... -Mode variant -Name 1 ``` @@ -210,6 +213,27 @@ DataParams: КлючВарианта="НоменклатураИЦены" Output: style=ЧерноБелый groups=Separately totalsH=None totalsV=None ``` +### trace — трассировка поля от заголовка до запроса + +Ищет поле по dataPath ИЛИ заголовку (включая подстроку) и показывает полную цепочку происхождения за один вызов: + +``` +=== Trace: КоэффициентКи "Коэффициент Ки" === + +Dataset: (schema-level only, not in dataset fields) + +Calculated: + ВЫБОР КОГДА ... ТОГДА 0 ИНАЧЕ ... КОНЕЦ + Operands: + КоличествоМесяцевИспользования -> РасчетНалогаНаИмущество [Query] + КоличествоМесяцевВладения -> РасчетНалогаНаИмущество [Query] + +Resource: + [ОсновноеСредство] Сумма(КоэффициентКи) +``` + +Типичный сценарий: пользователь видит колонку "Коэффициент Ки" в отчёте и спрашивает как она считается. Один вызов `trace` показывает: формулу вычисления, откуда берутся операнды, как агрегируется в ресурс. + ## Разрешение пути - Прямой путь: `path/to/Template.xml` @@ -232,6 +256,7 @@ Output: style=ЧерноБелый groups=Separately totalsH=None totalsV=Non - **Формулы и ресурсы**: totals для вычисляемых полей и ресурсов - **Программный вызов**: params для списка параметров - **Изменение вывода**: variant для структуры группировок и фильтров +- **Как считается колонка?**: trace для полной цепочки от заголовка до запроса ## Защита от переполнения diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index 26d855ea..91f01fe6 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -1,7 +1,7 @@ param( [Parameter(Mandatory=$true)] [string]$TemplatePath, - [ValidateSet("overview", "query", "fields", "links", "totals", "params", "variant")] + [ValidateSet("overview", "query", "fields", "links", "totals", "params", "variant", "trace")] [string]$Mode = "overview", [string]$Name, [int]$Batch = 0, @@ -516,6 +516,7 @@ if ($Mode -eq "overview") { } elseif ($variants.Count -gt 1) { $hints += "-Mode variant -Name variant structure (1..$($variants.Count))" } + $hints += "-Mode trace -Name trace field origin (by name or title)" $lines.Add("Next:") foreach ($h in $hints) { $lines.Add(" $h") } } @@ -1138,6 +1139,199 @@ elseif ($Mode -eq "variant") { } } +# ============================================================ +# 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)") + } + } +} + # --- Output --- $result = $lines.ToArray() From f211ffa2f02b8a823d483cdf4509b85b68bb82af Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 18:40:50 +0300 Subject: [PATCH 09/35] Split totals into calculated and resources modes Each concept now has its own mode with clear naming that matches the overview labels. Overview now shows "Resources:" instead of "Totals:" for consistency. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-info/SKILL.md | 48 +++--- .claude/skills/skd-info/scripts/skd-info.ps1 | 145 ++++++++++--------- 2 files changed, 103 insertions(+), 90 deletions(-) diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md index 1ab557d4..6e4b4249 100644 --- a/.claude/skills/skd-info/SKILL.md +++ b/.claude/skills/skd-info/SKILL.md @@ -1,7 +1,7 @@ --- name: skd-info description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты -argument-hint: [-Mode overview|query|fields|links|totals|params|variant|trace] [-Name ] +argument-hint: [-Mode overview|query|fields|links|calculated|resources|params|variant|trace] [-Name ] allowed-tools: - Bash - Read @@ -44,8 +44,10 @@ powershell.exe -NoProfile -File .claude\skills\skd-info\scripts\skd-info.ps1 -Te ... -Mode fields ... -Mode fields -Name НаборДанных1 ... -Mode links -... -Mode totals -... -Mode totals -Name СуммаНалога +... -Mode calculated +... -Mode calculated -Name КоэффициентКи +... -Mode resources +... -Mode resources -Name СуммаНалога ... -Mode params ... -Mode trace -Name КоэффициентКи ... -Mode trace -Name "Коэффициент Ки" @@ -73,7 +75,7 @@ Sources: ИсточникДанных1 (Local) Datasets: [Query] НоменклатураСЦенами 7 fields, query 40 lines Calculated: 1 -Totals: 1 +Resources: 1 Templates: 1 templates, 1 group bindings Params: (none) @@ -84,7 +86,8 @@ Variants: Next: -Mode query query text -Mode fields field tables by dataset - -Mode totals calculated fields + resources + -Mode calculated calculated field expressions + -Mode resources resource aggregation -Mode variant -Name variant structure (1..2) ``` @@ -152,15 +155,30 @@ Params: 18 (7 visible, 11 hidden): Период, Ответственный, ... Группирует по парам наборов. Показывает поля связи и параметры. -### totals — вычисляемые поля и ресурсы +### calculated — вычисляемые поля -Без `-Name` — карта: имена вычисляемых полей и ресурсов: +Без `-Name` — карта: имена и заголовки: ``` === Calculated fields (23) === ДоляСтоимости "Доля стоимости" КоэффициентКи "Коэффициент Ки" ... +``` +С `-Name <поле>` — полное выражение: +``` +=== Calculated: ДоляСтоимости === + +Expression: + ВЫБОР КОГДА ... ТОГДА "1" ИНАЧЕ ... КОНЕЦ +Title: Доля стоимости +Restrict: condition +``` + +### resources — ресурсы (итоги по группировкам) + +Без `-Name` — карта: имена полей, `*` = есть формулы по группировкам: +``` === Resources (51) === НалоговаяБаза КоэффициентКи * @@ -168,18 +186,11 @@ Params: 18 (7 visible, 11 hidden): Период, Ответственный, ... * = has group-level formulas ``` -С `-Name <поле>` — полная формула (если поле есть и в calculated, и в resources — покажет обе части): +С `-Name <поле>` — формулы агрегации: ``` -=== Calculated: КоэффициентКи === +=== Resource: ДатаСостояния === -Expression: - ВЫБОР КОГДА ... ТОГДА 0 ИНАЧЕ ... КОНЕЦ -Title: Коэффициент Ки -Restrict: condition - -=== Resource: КоэффициентКи === - - [ОсновноеСредство] Сумма(КоэффициентКи) + [ОсновноеСредство] ЕстьNull(ДатаСостояния, "") ``` ### params — параметры схемы @@ -253,7 +264,8 @@ Resource: - **Отладка данных**: query для просмотра текста запроса - **Модификация полей**: fields для списка с ролями по наборам - **Связи между наборами**: links для полей связи и параметров -- **Формулы и ресурсы**: totals для вычисляемых полей и ресурсов +- **Вычисляемые поля**: calculated для выражений вычисляемых полей +- **Ресурсы**: resources для формул агрегации по группировкам - **Программный вызов**: params для списка параметров - **Изменение вывода**: variant для структуры группировок и фильтров - **Как считается колонка?**: trace для полной цепочки от заголовка до запроса diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index 91f01fe6..fba8741e 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -1,7 +1,7 @@ param( [Parameter(Mandatory=$true)] [string]$TemplatePath, - [ValidateSet("overview", "query", "fields", "links", "totals", "params", "variant", "trace")] + [ValidateSet("overview", "query", "fields", "links", "calculated", "resources", "params", "variant", "trace")] [string]$Mode = "overview", [string]$Name, [int]$Batch = 0, @@ -385,9 +385,9 @@ if ($Mode -eq "overview") { } $groupNote = if ($hasGrouped) { ", with group formulas" } else { "" } if ($uniquePaths.Count -eq $totalFields.Count) { - $lines.Add("Totals: $($totalFields.Count)$groupNote") + $lines.Add("Resources: $($totalFields.Count)$groupNote") } else { - $lines.Add("Totals: $($totalFields.Count) ($($uniquePaths.Count) fields$groupNote)") + $lines.Add("Resources: $($totalFields.Count) ($($uniquePaths.Count) fields$groupNote)") } } @@ -505,8 +505,11 @@ if ($Mode -eq "overview") { } $calcCount = $root.SelectNodes("s:calculatedField", $ns).Count $totalCount = $root.SelectNodes("s:totalField", $ns).Count - if ($calcCount -gt 0 -or $totalCount -gt 0) { - $hints += "-Mode totals calculated fields + resources" + 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" @@ -827,18 +830,15 @@ elseif ($Mode -eq "links") { } # ============================================================ -# MODE: totals +# MODE: calculated # ============================================================ -elseif ($Mode -eq "totals") { +elseif ($Mode -eq "calculated") { $calcFields = $root.SelectNodes("s:calculatedField", $ns) - $totalFields = $root.SelectNodes("s:totalField", $ns) - - if ($Name) { - # Detail for specific field + if ($calcFields.Count -eq 0) { + $lines.Add("(no calculated fields)") + } elseif ($Name) { $found = $false - - # Search in calculated fields foreach ($cf in $calcFields) { $cfPath = $cf.SelectSingleNode("s:dataPath", $ns).InnerText if ($cfPath -eq $Name) { @@ -870,74 +870,75 @@ elseif ($Mode -eq "totals") { break } } - - # Search in resources (also show if already found in calculated — field can be both) - $matchedResources = @() - foreach ($tf in $totalFields) { - $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText - if ($tfPath -eq $Name) { $matchedResources += $tf } - } - if ($matchedResources.Count -gt 0) { - if ($found) { $lines.Add("") } - $lines.Add("=== Resource: $Name ===") - $lines.Add("") - foreach ($tf in $matchedResources) { - $tfExpr = $tf.SelectSingleNode("s:expression", $ns).InnerText - $tfGroup = $tf.SelectSingleNode("s:group", $ns) - $groupStr = "(overall)" - if ($tfGroup) { $groupStr = $tfGroup.InnerText } - $lines.Add(" [$groupStr] $tfExpr") - } - $found = $true - } - if (-not $found) { - Write-Error "Field '$Name' not found in calculated fields or resources" + Write-Error "Calculated field '$Name' not found" exit 1 } } else { - # Compact map - if ($calcFields.Count -gt 0) { - $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") + # 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("") + $lines.Add(" $cfPath$titleStr") } + $lines.Add("") + $lines.Add("Use -Name for full expression.") + } +} - if ($totalFields.Count -gt 0) { - $lines.Add("=== Resources ($($totalFields.Count)) ===") - # Collect unique field names with group info - $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") - } +# ============================================================ +# MODE: resources +# ============================================================ +elseif ($Mode -eq "resources") { - if ($calcFields.Count -eq 0 -and $totalFields.Count -eq 0) { - $lines.Add("(no calculated fields or resources)") - } else { - $lines.Add("") - $lines.Add("Use -Name for full expression.") + $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.") } } From dddb9e3dac4dea48fc10dd8e1f95a8b5c88e0bac Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 18:45:15 +0300 Subject: [PATCH 10/35] Make fields -Name drill into specific field for consistency fields -Name now takes a field dataPath (like calculated -Name and resources -Name) instead of a dataset name. Shows field detail: dataset, title, type, role, restrict, format, presentationExpression. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-info/SKILL.md | 13 +- .claude/skills/skd-info/scripts/skd-info.ps1 | 119 +++++++++++++++---- 2 files changed, 104 insertions(+), 28 deletions(-) diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md index 6e4b4249..afb7c834 100644 --- a/.claude/skills/skd-info/SKILL.md +++ b/.claude/skills/skd-info/SKILL.md @@ -25,7 +25,7 @@ allowed-tools: |--------------|:------------:|--------------|---------------------------------------------------| | TemplatePath | да | — | Путь к Template.xml или каталогу макета | | Mode | нет | `overview` | Режим: `overview`, `query`, `fields`, `links`, `totals`, `params`, `variant`, `trace` | -| Name | нет | — | Имя набора (query/fields), поля (totals) или варианта (variant) | +| Name | нет | — | Имя набора (query), поля (fields/calculated/resources/trace) или варианта (variant) | | Batch | нет | `0` | Номер пакета запроса (0 = все). Только для query | | Limit | нет | `150` | Макс. строк вывода (защита от переполнения) | | Offset | нет | `0` | Пропустить N строк (для пагинации) | @@ -134,13 +134,12 @@ Params: 18 (7 visible, 11 hidden): Период, Ответственный, ... РасчетНалогаНаИмущество [Query] (51): КадастроваяСтоимость, ... ``` -С `-Name <набор>` — полная таблица: +С `-Name <поле>` — детали конкретного поля: ``` -=== Fields: СостояниеОС [Query] (3) === - dataPath title role restrict format - Организация - - - - - ОсновноеСредство Объект - - - - ДатаСостояния Дата ввода в эксплуатацию - - ДФ=dd.MM.yyyy +=== Field: ДатаСостояния "Дата ввода в эксплуатацию" === + +Dataset: СостояниеОС [Query] +Format: ДФ=dd.MM.yyyy ``` ### links — связи наборов данных diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index fba8741e..0872fc80 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -731,33 +731,110 @@ elseif ($Mode -eq "fields") { } if ($Name) { - # Search nested items first (Union child over Union parent) + # 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) { - foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { - $subNameNode = $subDs.SelectSingleNode("s:name", $ns) - if ($subNameNode -and $subNameNode.InnerText -eq $Name) { - Show-DataSetFields $subDs - $found = $true - break - } - } - if ($found) { break } - } - if (-not $found) { - foreach ($ds in $dataSets) { - $dsNameNode = $ds.SelectSingleNode("s:name", $ns) - if ($dsNameNode -and $dsNameNode.InnerText -eq $Name) { - Show-DataSetFields $ds - $found = $true - break + $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 (-not $found) { - Write-Error "Dataset '$Name' not found" + + 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 ===") @@ -789,7 +866,7 @@ elseif ($Mode -eq "fields") { } $lines.Add("") - $lines.Add("Use -Name for full field table.") + $lines.Add("Use -Name for details.") } } From 0ca6d3f089428bdec4b9082903fa976f54da1ed6 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 18:52:20 +0300 Subject: [PATCH 11/35] Add variant list mode, fix stale SKILL.md references - variant without -Name now shows variant list (progressive disclosure) - Fix Mode parameter: totals -> calculated, resources - Fix fields -Name example: dataset name -> field name - Improved error message when variant not found Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-info/SKILL.md | 14 +++- .claude/skills/skd-info/scripts/skd-info.ps1 | 88 +++++++++++++++----- 2 files changed, 78 insertions(+), 24 deletions(-) diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md index afb7c834..b589b6be 100644 --- a/.claude/skills/skd-info/SKILL.md +++ b/.claude/skills/skd-info/SKILL.md @@ -24,7 +24,7 @@ allowed-tools: | Параметр | Обязательный | По умолчанию | Описание | |--------------|:------------:|--------------|---------------------------------------------------| | TemplatePath | да | — | Путь к Template.xml или каталогу макета | -| Mode | нет | `overview` | Режим: `overview`, `query`, `fields`, `links`, `totals`, `params`, `variant`, `trace` | +| Mode | нет | `overview` | Режим: `overview`, `query`, `fields`, `links`, `calculated`, `resources`, `params`, `variant`, `trace` | | Name | нет | — | Имя набора (query), поля (fields/calculated/resources/trace) или варианта (variant) | | Batch | нет | `0` | Номер пакета запроса (0 = все). Только для query | | Limit | нет | `150` | Макс. строк вывода (защита от переполнения) | @@ -42,7 +42,7 @@ powershell.exe -NoProfile -File .claude\skills\skd-info\scripts\skd-info.ps1 -Te ... -Mode query -Name НоменклатураСЦенами ... -Mode query -Name ДанныеТ13 -Batch 3 ... -Mode fields -... -Mode fields -Name НаборДанных1 +... -Mode fields -Name КадастроваяСтоимость ... -Mode links ... -Mode calculated ... -Mode calculated -Name КоэффициентКи @@ -202,8 +202,16 @@ Restrict: condition Организация CatalogRef.Организации null yes - ``` -### variant — структура варианта +### variant — варианты отчёта +Без `-Name` — список вариантов: +``` +=== Variants (2) === + [1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters + [2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters +``` + +С `-Name ` — структура конкретного варианта: ``` === Variant [1]: НоменклатураИЦены "Номенклатура и цены" === diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index 0872fc80..70541a19 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -1091,32 +1091,77 @@ elseif ($Mode -eq "variant") { $variants = $root.SelectNodes("s:settingsVariant", $ns) - $targetVariant = $null - if ($Name) { - # Try by name - $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 $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") } } } - if (-not $targetVariant) { - Write-Error "Variant '$Name' not found" - exit 1 - } } else { - # Take first - $targetVariant = $variants[0] - $matchIdx = 1 - if (-not $targetVariant) { - Write-Error "No variants found" - exit 1 + # --- 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) @@ -1215,6 +1260,7 @@ elseif ($Mode -eq "variant") { } } } + } # end else (variant detail) } # ============================================================ From d72dd1678e2aec6f8190c1c02f157edef37f4b42 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 19:01:16 +0300 Subject: [PATCH 12/35] Extract detailed mode examples into modes-reference.md SKILL.md: 288 -> 72 lines. Compact table of modes with progressive disclosure pattern, typical workflow, command reference. Detailed output examples moved to modes-reference.md (loaded on demand via Read tool, not on every skill invocation). Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-info/SKILL.md | 284 +++------------------ .claude/skills/skd-info/modes-reference.md | 204 +++++++++++++++ 2 files changed, 238 insertions(+), 250 deletions(-) create mode 100644 .claude/skills/skd-info/modes-reference.md diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md index b589b6be..7756f0d9 100644 --- a/.claude/skills/skd-info/SKILL.md +++ b/.claude/skills/skd-info/SKILL.md @@ -12,26 +12,16 @@ allowed-tools: Читает Template.xml схемы компоновки данных (СКД) и выводит компактную сводку. Заменяет необходимость читать тысячи строк XML. -## Использование +## Параметры и команда -``` -/skd-info -/skd-info -Mode query -Name НаборДанных1 -``` - -## Параметры - -| Параметр | Обязательный | По умолчанию | Описание | -|--------------|:------------:|--------------|---------------------------------------------------| -| TemplatePath | да | — | Путь к Template.xml или каталогу макета | -| Mode | нет | `overview` | Режим: `overview`, `query`, `fields`, `links`, `calculated`, `resources`, `params`, `variant`, `trace` | -| Name | нет | — | Имя набора (query), поля (fields/calculated/resources/trace) или варианта (variant) | -| Batch | нет | `0` | Номер пакета запроса (0 = все). Только для query | -| Limit | нет | `150` | Макс. строк вывода (защита от переполнения) | -| Offset | нет | `0` | Пропустить N строк (для пагинации) | -| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | - -## Команда +| Параметр | Описание | +|----------|----------| +| `TemplatePath` | Путь к Template.xml или каталогу макета (авто-резолв в `Ext/Template.xml`) | +| `Mode` | Режим анализа (по умолчанию `overview`) | +| `Name` | Имя набора (query), поля (fields/calculated/resources/trace) или варианта (variant) | +| `Batch` | Номер пакета запроса, 0 = все (только query) | +| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | +| `OutFile` | Записать результат в файл (UTF-8 BOM) | ```powershell powershell.exe -NoProfile -File .claude\skills\skd-info\scripts\skd-info.ps1 -TemplatePath "<путь>" @@ -41,247 +31,41 @@ powershell.exe -NoProfile -File .claude\skills\skd-info\scripts\skd-info.ps1 -Te ```powershell ... -Mode query -Name НоменклатураСЦенами ... -Mode query -Name ДанныеТ13 -Batch 3 -... -Mode fields ... -Mode fields -Name КадастроваяСтоимость -... -Mode links -... -Mode calculated ... -Mode calculated -Name КоэффициентКи -... -Mode resources ... -Mode resources -Name СуммаНалога -... -Mode params -... -Mode trace -Name КоэффициентКи ... -Mode trace -Name "Коэффициент Ки" -... -Mode variant -Name Основной ... -Mode variant -Name 1 ``` -Для большого вывода используй `-OutFile`: -```powershell -... -Mode query -Name ДанныеТ13 -OutFile test-tmp\query.txt -``` -Затем прочитай результат через Read tool. - ## Режимы -### overview (по умолчанию) — карта схемы +| Режим | Без `-Name` | С `-Name` | +|-------|-------------|-----------| +| `overview` | Навигационная карта схемы + подсказки Next | — | +| `query` | — | Текст запроса набора (с оглавлением батчей) | +| `fields` | Карта: имена полей по наборам | Деталь поля: набор, тип, роль, формат | +| `links` | Все связи наборов | — | +| `calculated` | Карта: имена вычисляемых полей | Выражение + заголовок + ограничения | +| `resources` | Карта: имена ресурсов (`*` = групповые формулы) | Формулы агрегации по группировкам | +| `params` | Таблица параметров: тип, значение, видимость | — | +| `variant` | Список вариантов | Структура группировок + фильтры + вывод | +| `trace` | — | Полная цепочка: набор → вычисление → ресурс | -Компактная навигационная карта (10-25 строк). Показывает структуру и подсказывает следующие шаги: +Паттерн: без `-Name` — карта/индекс, с `-Name` — деталь конкретного элемента. + +## Типичный workflow + +1. `overview` — понять структуру, увидеть подсказки +2. `trace -Name <поле>` — узнать как считается колонка отчёта (от заголовка до запроса за один вызов) +3. `query -Name <набор>` — посмотреть текст SQL-запроса +4. `variant -Name ` — посмотреть группировки и фильтры варианта + +Подробные примеры вывода каждого режима — в `modes-reference.md`. + +## Верификация ``` -=== 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) +/skd-info — overview (точка входа) +/skd-info -Mode trace -Name — трассировка поля ``` - -Для 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 — текст запроса - -Извлекает raw-текст запроса с деэкранированием XML (`&`→`&`, `>`→`>`). Для пакетных запросов — оглавление батчей: - -``` -=== Query: ДанныеТ13 (334 lines, 10 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 -``` - -### links — связи наборов данных - -``` -=== Links (5) === - -РасчетНалогаНаИмущество -> СостояниеОС : - Организация -> Организация - ОсновноеСредство -> ОсновноеСредство -``` - -Группирует по парам наборов. Показывает поля связи и параметры. - -### 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 -``` - -### trace — трассировка поля от заголовка до запроса - -Ищет поле по dataPath ИЛИ заголовку (включая подстроку) и показывает полную цепочку происхождения за один вызов: - -``` -=== Trace: КоэффициентКи "Коэффициент Ки" === - -Dataset: (schema-level only, not in dataset fields) - -Calculated: - ВЫБОР КОГДА ... ТОГДА 0 ИНАЧЕ ... КОНЕЦ - Operands: - КоличествоМесяцевИспользования -> РасчетНалогаНаИмущество [Query] - КоличествоМесяцевВладения -> РасчетНалогаНаИмущество [Query] - -Resource: - [ОсновноеСредство] Сумма(КоэффициентКи) -``` - -Типичный сценарий: пользователь видит колонку "Коэффициент Ки" в отчёте и спрашивает как она считается. Один вызов `trace` показывает: формулу вычисления, откуда берутся операнды, как агрегируется в ресурс. - -## Разрешение пути - -- Прямой путь: `path/to/Template.xml` -- Каталог макета: `path/to/ИмяМакета/` → авто-резолв в `Ext/Template.xml` - -## Что не выводится - -- XML namespace-декларации -- Обёртки v8:item/v8:lang/v8:content (извлекаем чистый текст) -- userSettingID (GUID-ы пользовательских настроек) -- Дефолтные periodAdditionBegin/End = 0001-01-01 -- viewMode - -## Когда использовать - -- **Перед анализом отчёта**: overview для понимания структуры -- **Отладка данных**: query для просмотра текста запроса -- **Модификация полей**: fields для списка с ролями по наборам -- **Связи между наборами**: links для полей связи и параметров -- **Вычисляемые поля**: calculated для выражений вычисляемых полей -- **Ресурсы**: resources для формул агрегации по группировкам -- **Программный вызов**: params для списка параметров -- **Изменение вывода**: variant для структуры группировок и фильтров -- **Как считается колонка?**: trace для полной цепочки от заголовка до запроса - -## Защита от переполнения - -Вывод ограничен 150 строками по умолчанию. При превышении: -``` -[TRUNCATED] Shown 150 of 400 lines. Use -Offset 150 to continue. -``` - -Используйте `-Offset N` и `-Limit N` для постраничного просмотра. diff --git a/.claude/skills/skd-info/modes-reference.md b/.claude/skills/skd-info/modes-reference.md new file mode 100644 index 00000000..8780cf10 --- /dev/null +++ b/.claude/skills/skd-info/modes-reference.md @@ -0,0 +1,204 @@ +# /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 +``` + +## 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 From c306d18648268b57cd38577a809f4e344fa91a08 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 19:56:56 +0300 Subject: [PATCH 13/35] Add templates mode for DCS template binding analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Map (without -Name): field bindings with non-trivial expression detection, group bindings organized by group name (groupTemplate, groupHeaderTemplate, groupFooterTemplate, fieldTemplate) - Detail (-Name ): template content with rows, cells (static text and parameters), non-trivial expressions only - Trivial filter: Field=Field and Field=Представление(Field) hidden - Updated overview: shows binding type counts, templates hint in Next Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-info/SKILL.md | 7 +- .claude/skills/skd-info/modes-reference.md | 42 +++ .claude/skills/skd-info/scripts/skd-info.ps1 | 318 ++++++++++++++++++- 3 files changed, 357 insertions(+), 10 deletions(-) diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md index 7756f0d9..4acc6019 100644 --- a/.claude/skills/skd-info/SKILL.md +++ b/.claude/skills/skd-info/SKILL.md @@ -1,7 +1,7 @@ --- name: skd-info description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты -argument-hint: [-Mode overview|query|fields|links|calculated|resources|params|variant|trace] [-Name ] +argument-hint: [-Mode overview|query|fields|links|calculated|resources|params|variant|templates|trace] [-Name ] allowed-tools: - Bash - Read @@ -18,7 +18,7 @@ allowed-tools: |----------|----------| | `TemplatePath` | Путь к Template.xml или каталогу макета (авто-резолв в `Ext/Template.xml`) | | `Mode` | Режим анализа (по умолчанию `overview`) | -| `Name` | Имя набора (query), поля (fields/calculated/resources/trace) или варианта (variant) | +| `Name` | Имя набора (query), поля (fields/calculated/resources/trace), варианта (variant) или группировки/поля (templates) | | `Batch` | Номер пакета запроса, 0 = все (только query) | | `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | | `OutFile` | Записать результат в файл (UTF-8 BOM) | @@ -36,6 +36,8 @@ powershell.exe -NoProfile -File .claude\skills\skd-info\scripts\skd-info.ps1 -Te ... -Mode resources -Name СуммаНалога ... -Mode trace -Name "Коэффициент Ки" ... -Mode variant -Name 1 +... -Mode templates +... -Mode templates -Name ВидНалоговойБазы ``` ## Режимы @@ -50,6 +52,7 @@ powershell.exe -NoProfile -File .claude\skills\skd-info\scripts\skd-info.ps1 -Te | `resources` | Карта: имена ресурсов (`*` = групповые формулы) | Формулы агрегации по группировкам | | `params` | Таблица параметров: тип, значение, видимость | — | | `variant` | Список вариантов | Структура группировок + фильтры + вывод | +| `templates` | Карта привязок шаблонов (field/group) | Содержимое шаблона: строки, ячейки, выражения | | `trace` | — | Полная цепочка: набор → вычисление → ресурс | Паттерн: без `-Name` — карта/индекс, с `-Name` — деталь конкретного элемента. diff --git a/.claude/skills/skd-info/modes-reference.md b/.claude/skills/skd-info/modes-reference.md index 8780cf10..eb6bd983 100644 --- a/.claude/skills/skd-info/modes-reference.md +++ b/.claude/skills/skd-info/modes-reference.md @@ -174,6 +174,48 @@ 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 ИЛИ заголовку (включая подстроку) и показывает полную цепочку происхождения за один вызов: diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index 70541a19..636903b8 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -1,7 +1,7 @@ param( [Parameter(Mandatory=$true)] [string]$TemplatePath, - [ValidateSet("overview", "query", "fields", "links", "calculated", "resources", "params", "variant", "trace")] + [ValidateSet("overview", "query", "fields", "links", "calculated", "resources", "params", "variant", "trace", "templates")] [string]$Mode = "overview", [string]$Name, [int]$Batch = 0, @@ -44,6 +44,7 @@ $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 @@ -391,14 +392,19 @@ if ($Mode -eq "overview") { } } - # Templates — count only - $templates = $root.SelectNodes("s:template", $ns) - $groupTemplates = $root.SelectNodes("s:groupTemplate", $ns) - if ($templates.Count -gt 0 -or $groupTemplates.Count -gt 0) { + # 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 ($templates.Count -gt 0) { $parts += "$($templates.Count) templates" } - if ($groupTemplates.Count -gt 0) { $parts += "$($groupTemplates.Count) group bindings" } - $lines.Add("Templates: " + ($parts -join ", ")) + 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 @@ -519,6 +525,9 @@ if ($Mode -eq "overview") { } 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") } @@ -1456,6 +1465,299 @@ elseif ($Mode -eq "trace") { } } +# ============================================================ +# 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() From d573f84c9d24dbf8101b2ea6e55f3606e88ee6b7 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 21:27:19 +0300 Subject: [PATCH 14/35] Add skd-compile and skd-validate skills for DCS schema generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - skd-compile: JSON DSL → Template.xml (DataCompositionSchema) Shorthand parsers for fields, totals, parameters, calculated fields. Full type system, settings variants with selection/filter/order/structure. - skd-validate: structural validation of Template.xml (~30 checks) DataSources, DataSets, fields, links, params, templates, variants. - docs/skd-dsl-spec.md: full DSL specification Tested on compiled examples and 5+ real DCS from acc_8.3.24 (0 errors). Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-compile/SKILL.md | 131 ++ .../skd-compile/scripts/skd-compile.ps1 | 1185 +++++++++++++++++ .claude/skills/skd-validate/SKILL.md | 74 + .../skd-validate/scripts/skd-validate.ps1 | 717 ++++++++++ docs/skd-dsl-spec.md | 639 +++++++++ 5 files changed, 2746 insertions(+) create mode 100644 .claude/skills/skd-compile/SKILL.md create mode 100644 .claude/skills/skd-compile/scripts/skd-compile.ps1 create mode 100644 .claude/skills/skd-validate/SKILL.md create mode 100644 .claude/skills/skd-validate/scripts/skd-validate.ps1 create mode 100644 docs/skd-dsl-spec.md diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md new file mode 100644 index 00000000..38a21d6d --- /dev/null +++ b/.claude/skills/skd-compile/SKILL.md @@ -0,0 +1,131 @@ +--- +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`. + +Роли: `@dimension`, `@account`, `@balance`, `@period`. + +Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`. + +### Итоги (shorthand) + +```json +"totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"] +``` + +### Параметры (shorthand) + +```json +"parameters": ["Период: StandardPeriod = LastMonth", "Организация: CatalogRef.Организации"] +``` + +### Варианты настроек + +```json +"settingsVariants": [{ + "name": "Основной", + "presentation": "Основной", + "settings": { + "selection": ["Наименование", "Количество", "Auto"], + "filter": [{ "field": "Организация", "op": "=", "use": false, "userSettingID": "auto" }], + "order": ["Количество desc", "Auto"], + "outputParameters": { "Заголовок": "Мой отчёт" }, + "structure": [{ "type": "group", "groupBy": ["Организация"], "selection": ["Auto"], "order": ["Auto"], + "children": [{ "type": "group", "selection": ["Auto"], "order": ["Auto"] }] + }] + } +}] +``` + +## Примеры + +### Минимальный + +```json +{ + "dataSets": [{ + "query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура", + "fields": ["Наименование"] + }] +} +``` + +### С ресурсами и параметрами + +```json +{ + "dataSets": [{ + "query": "ВЫБРАТЬ Продажи.Номенклатура, Продажи.Количество, Продажи.Сумма ИЗ РегистрНакопления.Продажи КАК Продажи", + "fields": ["Номенклатура: CatalogRef.Номенклатура @dimension", "Количество: decimal(15,3)", "Сумма: decimal(15,2)"] + }], + "totalFields": ["Количество: Сумма", "Сумма: Сумма"], + "parameters": ["Период: StandardPeriod = LastMonth"] +} +``` + +## Верификация + +``` +/skd-validate — валидация структуры 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..72fd6e0e --- /dev/null +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -0,0 +1,1185 @@ +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" +} + +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 --- + +function Emit-ValueType { + param([string]$typeStr, [string]$indent) + + if (-not $typeStr) { return } + + # 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 + } + + # cfg: references (CatalogRef.XXX, DocumentRef.XXX, EnumRef.XXX, etc.) + if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { + X "$indentcfg:$typeStr" + return + } + + # Fallback + if ($typeStr.Contains('.')) { + X "$indentcfg:$typeStr" + } else { + X "$indent$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 = $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 } + + # Split "Name: Type = Value" + if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.+))?$') { + $result.name = $Matches[1].Trim() + $result.type = $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 = "" } +} + +# --- 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$($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) { "$($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" + } + } + 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" + } + } + X "$indent`t" + } + + # Role + if ($f.roles.Count -gt 0 -or $f["roleObj"]) { + X "$indent`t" + foreach ($role in $f.roles) { + 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$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) { + X "`t`t" + Emit-ValueType -typeStr "$($cf.type)" -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" } + } + 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.group = "$($tf.group)" } + } + + X "`t" + X "`t`t$(Esc-Xml $parsed.dataPath)" + X "`t`t$(Esc-Xml $parsed.expression)" + if ($parsed.group) { + X "`t`t$(Esc-Xml $parsed.group)" + } + X "`t" + } +} + +# === Parameters === +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) { "$($p.type)" } else { "" } + value = $p.value + } + } + + 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 ($p -isnot [string] -and $p.expression) { + X "`t`t$(Esc-Xml "$($p.expression)")" + } + + # AvailableAsField + if ($p -isnot [string] -and $p.availableAsField -eq $false) { + X "`t`tfalse" + } + + # Use + if ($p -isnot [string] -and $p.use) { + X "`t`t$($p.use)" + } + + X "`t" + } +} + +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$valStr" + X "$indent" + } elseif ($type -match '^date') { + X "$indent$valStr" + } elseif ($type -eq "boolean") { + X "$indent$valStr" + } elseif ($type -match '^decimal') { + X "$indent$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$valStr" + } elseif ($valStr -eq "true" -or $valStr -eq "false") { + X "$indent$valStr" + } else { + X "$indent$(Esc-Xml $valStr)" + } + } +} + +# === Templates === +function Emit-Templates { + if (-not $def.templates) { return } + foreach ($t in $def.templates) { + 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$($gt.templateType)" + X "`t`t" + X "`t" + } +} + +# === Settings Variants === + +function Emit-Selection { + param($items, [string]$indent) + + if (-not $items -or $items.Count -eq 0) { return } + + X "$indent" + foreach ($item in $items) { + if ($item -is [string]) { + if ($item -eq "Auto") { + X "$indent`t" + } else { + X "$indent`t" + X "$indent`t`t$item" + X "$indent`t" + } + } else { + X "$indent`t" + X "$indent`t`t$($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$($item.field)" + + $compType = $script:comparisonTypes["$($item.op)"] + if (-not $compType) { $compType = "$($item.op)" } + X "$indent`t$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$($item.viewMode)" + } + + if ($item.userSettingID) { + $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } + X "$indent`t$uid" + } + + X "$indent" +} + +function Emit-Filter { + param($items, [string]$indent) + + if (-not $items -or $items.Count -eq 0) { return } + + X "$indent" + foreach ($item in $items) { + Emit-FilterItem -item $item -indent "$indent`t" + } + X "$indent" +} + +function Emit-Order { + param($items, [string]$indent) + + if (-not $items -or $items.Count -eq 0) { return } + + X "$indent" + foreach ($item in $items) { + if ($item -is [string]) { + if ($item -eq "Auto") { + 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$field" + X "$indent`t`t$dir" + 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) { + 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.variant) { + # StandardPeriod + X "$indent`t`t" + X "$indent`t`t`t$($dp.value.variant)" + X "$indent`t`t" + } elseif ($dp.value -is [bool]) { + $bv = "$($dp.value)".ToLower() + X "$indent`t`t$bv" + } elseif ("$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') { + X "$indent`t`t$($dp.value)" + } else { + X "$indent`t`t$(Esc-Xml "$($dp.value)")" + } + } + + if ($dp.viewMode) { + X "$indent`t`t$($dp.viewMode)" + } + + if ($dp.userSettingID) { + $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" } + X "$indent`t`t$uid" + } + + 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$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$($field.field)" + $gt = if ($field.groupType) { "$($field.groupType)" } else { "Items" } + X "$indent`t`t$gt" + $pat = if ($field.periodAdditionType) { "$($field.periodAdditionType)" } else { "None" } + X "$indent`t`t$pat" + X "$indent`t`t0001-01-01T00:00:00" + X "$indent`t`t0001-01-01T00:00:00" + X "$indent`t" + } + } + X "$indent" +} + +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" + Emit-Order -items $item.order -indent "$indent`t" + Emit-Selection -items $item.selection -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" + Emit-Order -items $col.order -indent "$indent`t`t" + Emit-Selection -items $col.selection -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" + Emit-Order -items $row.order -indent "$indent`t`t" + Emit-Selection -items $row.selection -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" + Emit-Order -items $item.points.order -indent "$indent`t`t" + Emit-Selection -items $item.points.selection -indent "$indent`t`t" + X "$indent`t" + } + + # Series + if ($item.series) { + X "$indent`t" + Emit-GroupItems -groupBy $item.series.groupBy -indent "$indent`t`t" + Emit-Order -items $item.series.order -indent "$indent`t`t" + Emit-Selection -items $item.series.selection -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 + if ($s.selection) { + Emit-Selection -items $s.selection -indent "`t`t`t" + } + + # Filter + if ($s.filter) { + Emit-Filter -items $s.filter -indent "`t`t`t" + } + + # Order + if ($s.order) { + Emit-Order -items $s.order -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 + if ($s.structure) { + foreach ($item in $s.structure) { + 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-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..1a886ca6 --- /dev/null +++ b/.claude/skills/skd-validate/scripts/skd-validate.ps1 @@ -0,0 +1,717 @@ +param( + [Parameter(Mandatory)] + [string]$TemplatePath, + + [int]$MaxErrors = 20, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- + +if (-not $TemplatePath.EndsWith(".xml")) { + $candidate = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml" + if (Test-Path $candidate) { + $TemplatePath = $candidate + } +} + +if (-not (Test-Path $TemplatePath)) { + Write-Error "File not found: $TemplatePath" + exit 1 +} + +$resolvedPath = (Resolve-Path $TemplatePath).Path +$fileName = [System.IO.Path]::GetFileName($resolvedPath) + +# --- Output infrastructure --- + +$script:errors = 0 +$script:warnings = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 4096 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + Out-Line "[OK] $msg" +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ===" + + $result = $script:output.ToString() + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +Out-Line "=== Validation: $fileName ===" +Out-Line "" + +# --- 1. Parse XML --- + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) + Report-OK "XML parsed successfully" +} catch { + Report-Error "XML parse failed: $($_.Exception.Message)" + # Cannot continue + $result = $script:output.ToString() + Write-Host $result + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + } + exit 1 +} + +# --- 2. Register namespaces --- + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("s", "http://v8.1c.ru/8.1/data-composition-system/schema") +$ns.AddNamespace("dcscom", "http://v8.1c.ru/8.1/data-composition-system/common") +$ns.AddNamespace("dcscor", "http://v8.1c.ru/8.1/data-composition-system/core") +$ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("dcsat", "http://v8.1c.ru/8.1/data-composition-system/area-template") + +$root = $xmlDoc.DocumentElement + +# --- 3. Root element checks --- + +if ($root.LocalName -ne "DataCompositionSchema") { + Report-Error "Root element is '$($root.LocalName)', expected 'DataCompositionSchema'" +} else { + Report-OK "Root element: DataCompositionSchema" +} + +$expectedNs = "http://v8.1c.ru/8.1/data-composition-system/schema" +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "Default namespace is '$($root.NamespaceURI)', expected '$expectedNs'" +} else { + Report-OK "Default namespace correct" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 4. Collect inventories --- + +# DataSources +$dataSourceNodes = $root.SelectNodes("s:dataSource", $ns) +$dataSourceNames = @{} +foreach ($dsn in $dataSourceNodes) { + $name = $dsn.SelectSingleNode("s:name", $ns) + if ($name) { $dataSourceNames[$name.InnerText] = $true } +} + +# DataSets (recursive for unions) +$dataSetNodes = $root.SelectNodes("s:dataSet", $ns) +$dataSetNames = @{} +$allFieldPaths = @{} # Global: dataPath → dataSet name + +function Collect-DataSetFields { + param($dsNode, [string]$dsName) + + $fields = $dsNode.SelectNodes("s:field", $ns) + $localPaths = @{} + foreach ($f in $fields) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + if ($dp) { + $path = $dp.InnerText + $localPaths[$path] = $true + $allFieldPaths[$path] = $dsName + } + } + + # Union items + $items = $dsNode.SelectNodes("s:item", $ns) + foreach ($item in $items) { + $itemName = $item.SelectSingleNode("s:name", $ns) + if ($itemName) { + Collect-DataSetFields -dsNode $item -dsName $itemName.InnerText + } + } + + return $localPaths +} + +$dataSetFieldMap = @{} # dsName → hashtable of dataPath +foreach ($ds in $dataSetNodes) { + $nameNode = $ds.SelectSingleNode("s:name", $ns) + if ($nameNode) { + $dsName = $nameNode.InnerText + $dataSetNames[$dsName] = $true + $dataSetFieldMap[$dsName] = Collect-DataSetFields -dsNode $ds -dsName $dsName + } +} + +# CalculatedFields +$calcFieldNodes = $root.SelectNodes("s:calculatedField", $ns) +$calcFieldPaths = @{} +foreach ($cf in $calcFieldNodes) { + $dp = $cf.SelectSingleNode("s:dataPath", $ns) + if ($dp) { $calcFieldPaths[$dp.InnerText] = $true } +} + +# TotalFields +$totalFieldNodes = $root.SelectNodes("s:totalField", $ns) + +# Parameters +$paramNodes = $root.SelectNodes("s:parameter", $ns) +$paramNames = @{} +foreach ($p in $paramNodes) { + $nameNode = $p.SelectSingleNode("s:name", $ns) + if ($nameNode) { $paramNames[$nameNode.InnerText] = $true } +} + +# Templates +$templateNodes = $root.SelectNodes("s:template", $ns) +$templateNames = @{} +foreach ($t in $templateNodes) { + $nameNode = $t.SelectSingleNode("s:name", $ns) + if ($nameNode) { $templateNames[$nameNode.InnerText] = $true } +} + +# GroupTemplates +$groupTemplateNodes = $root.SelectNodes("s:groupTemplate", $ns) + +# SettingsVariants +$variantNodes = $root.SelectNodes("s:settingsVariant", $ns) + +# Known fields = dataset fields + calculated fields +$knownFields = @{} +foreach ($key in $allFieldPaths.Keys) { $knownFields[$key] = $true } +foreach ($key in $calcFieldPaths.Keys) { $knownFields[$key] = $true } + +# --- 5. DataSource checks --- + +if ($dataSourceNodes.Count -eq 0) { + Report-Warn "No dataSource elements found (settings-only DCS?)" +} else { + $dsNamesSeen = @{} + $dsOk = $true + foreach ($dsn in $dataSourceNodes) { + $name = $dsn.SelectSingleNode("s:name", $ns) + $type = $dsn.SelectSingleNode("s:dataSourceType", $ns) + if (-not $name -or -not $name.InnerText) { + Report-Error "DataSource has empty name" + $dsOk = $false + } elseif ($dsNamesSeen.ContainsKey($name.InnerText)) { + Report-Error "Duplicate dataSource name: $($name.InnerText)" + $dsOk = $false + } else { + $dsNamesSeen[$name.InnerText] = $true + } + if ($type) { + $tv = $type.InnerText + if ($tv -ne "Local" -and $tv -ne "External") { + Report-Warn "DataSource '$($name.InnerText)' has unusual type: $tv" + } + } + } + if ($dsOk) { + Report-OK "$($dataSourceNodes.Count) dataSource(s) found, names unique" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 6. DataSet checks --- + +$validDsTypes = @("DataSetQuery", "DataSetObject", "DataSetUnion") + +if ($dataSetNodes.Count -eq 0) { + Report-Warn "No dataSet elements found (settings-only DCS?)" +} else { + $dsNamesSeen = @{} + $dsOk = $true + foreach ($ds in $dataSetNodes) { + $xsiType = $ds.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + $nameNode = $ds.SelectSingleNode("s:name", $ns) + $dsName = if ($nameNode) { $nameNode.InnerText } else { "(unnamed)" } + + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "DataSet has empty name" + $dsOk = $false + } elseif ($dsNamesSeen.ContainsKey($dsName)) { + Report-Error "Duplicate dataSet name: $dsName" + $dsOk = $false + } else { + $dsNamesSeen[$dsName] = $true + } + + if (-not $xsiType) { + Report-Error "DataSet '$dsName' missing xsi:type" + $dsOk = $false + } elseif ($validDsTypes -notcontains $xsiType) { + Report-Warn "DataSet '$dsName' has unusual xsi:type: $xsiType" + } + + # Check dataSource reference + if ($xsiType -ne "DataSetUnion") { + $srcNode = $ds.SelectSingleNode("s:dataSource", $ns) + if ($srcNode -and $srcNode.InnerText) { + if (-not $dataSourceNames.ContainsKey($srcNode.InnerText)) { + Report-Error "DataSet '$dsName' references unknown dataSource: $($srcNode.InnerText)" + $dsOk = $false + } + } + } + + # Check query not empty for Query type + if ($xsiType -eq "DataSetQuery") { + $queryNode = $ds.SelectSingleNode("s:query", $ns) + if (-not $queryNode -or -not $queryNode.InnerText.Trim()) { + Report-Warn "DataSet '$dsName' (Query) has empty query" + } + } + + # Check objectName for Object type + if ($xsiType -eq "DataSetObject") { + $objNode = $ds.SelectSingleNode("s:objectName", $ns) + if (-not $objNode -or -not $objNode.InnerText.Trim()) { + Report-Error "DataSet '$dsName' (Object) has empty objectName" + $dsOk = $false + } + } + } + + if ($dsOk) { + Report-OK "$($dataSetNodes.Count) dataSet(s) found, names unique" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 7. Field checks --- + +function Check-DataSetFields { + param($dsNode, [string]$dsName) + + $fields = $dsNode.SelectNodes("s:field", $ns) + if ($fields.Count -eq 0) { return } + + $pathsSeen = @{} + $fieldOk = $true + + foreach ($f in $fields) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + $fn = $f.SelectSingleNode("s:field", $ns) + + if (-not $dp -or -not $dp.InnerText) { + Report-Error "DataSet '$dsName': field has empty dataPath" + $fieldOk = $false + continue + } + + $path = $dp.InnerText + if ($pathsSeen.ContainsKey($path)) { + Report-Warn "DataSet '$dsName': duplicate dataPath '$path'" + } else { + $pathsSeen[$path] = $true + } + + if (-not $fn -or -not $fn.InnerText) { + Report-Warn "DataSet '$dsName': field '$path' has empty 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/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md new file mode 100644 index 00000000..52c3eb6c --- /dev/null +++ b/docs/skd-dsl-spec.md @@ -0,0 +1,639 @@ +# 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` | `cfg:CatalogRef.XXX` | — | +| `DocumentRef.XXX` | `cfg:DocumentRef.XXX` | — | +| `EnumRef.XXX` | `cfg:EnumRef.XXX` | — | +| `ChartOfAccountsRef.XXX` | `cfg:ChartOfAccountsRef.XXX` | — | +| `StandardPeriod` | `v8:StandardPeriod` | — | + +### Роли + +| 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}` | `true` | + +Объектная форма с доп. полями: +```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" } +``` + +--- + +## 6. Параметры (parameters) + +### Shorthand + +``` +": [= ]" +``` + +Примеры: + +```json +"parameters": [ + "Период: StandardPeriod = LastMonth", + "Организация: CatalogRef.Организации", + "ДатаОтчета: date" +] +``` + +**Парсинг:** `"A: T = V"` → `name=A`, `type=T`, `value=V`. Значение `LastMonth` и другие варианты периодов → `v8:StandardPeriod` с `v8:variant`. + +### Объектная форма + +```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": [...], + "outputParameters": {...}, + "dataParameters": [...], + "structure": [...] + } +}] +``` + +### selection + +```json +"selection": [ + "Наименование", + { "field": "Количество", "title": "Кол-во" }, + "Auto" +] +``` + +- Строка → `SelectedItemField` +- `"Auto"` → `SelectedItemAuto` +- Объект с `field`/`title` → `SelectedItemField` с `lwsTitle` + +### filter + +```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 | + +Операторы: + +| 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` + +### outputParameters + +```json +"outputParameters": { + "Заголовок": "Мой отчёт", + "ВыводитьЗаголовок": "Output", + "МакетОформления": "ОформлениеОтчетовЧерноБелый" +} +``` + +Ключ → `dcscor:parameter`, значение → `dcscor:value`. + +Типы значений определяются автоматически: +- `"Заголовок"` → `v8:LocalStringType` +- `"ВыводитьЗаголовок"`, `"ВыводитьПараметрыДанных"`, `"ВыводитьОтбор"` → `dcsset:DataCompositionTextOutputType` +- `"РасположениеПолейГруппировки"` → `dcsset:DataCompositionGroupFieldsPlacement` +- `"РасположениеРеквизитов"` → `dcsset:DataCompositionAttributesPlacement` +- `"ГоризонтальноеРасположениеОбщихИтогов"`, `"ВертикальноеРасположениеОбщихИтогов"` → `dcscor:DataCompositionTotalPlacement` +- Прочие → `xs:string` + +### dataParameters + +```json +"dataParameters": [ + { "parameter": "Период", "value": { "variant": "LastMonth" }, "userSettingID": "auto" }, + { "parameter": "Организация", "use": false, "viewMode": "Normal", "userSettingID": "auto" } +] +``` + +### structure + +```json +"structure": [ + { + "type": "group", + "groupBy": ["Организация"], + "selection": ["Auto"], + "order": ["Auto"], + "children": [ + { "type": "group", "selection": ["Auto"], "order": ["Auto"] } + ] + } +] +``` + +#### Группировка (group) + +| Поле | Описание | +|------|----------| +| `type` | `"group"` | +| `name` | Имя группировки (опц.) | +| `groupBy` | Массив полей. Пусто/опущено = детальные записи | +| `groupType` | `"Items"` (умолч.), `"Hierarchy"`, `"HierarchyOnly"` | +| `selection` | Выборка (как в settings) | +| `filter` | Отборы (как в settings) | +| `order` | Сортировка (как в settings) | +| `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": "Основной", + "presentation": "Основной", + "settings": { + "selection": ["Наименование", "Количество"], + "structure": [ + { "type": "group", "order": ["Auto"], "selection": ["Auto"] } + ] + } + }] +} +``` + +## 12. Полный пример — средний + +```json +{ + "dataSets": [ + { + "name": "Продажи", + "query": "ВЫБРАТЬ\n\tПродажи.Организация,\n\tПродажи.Номенклатура,\n\tПродажи.Количество,\n\tПродажи.Сумма\nИЗ\n\tРегистрНакопления.Продажи КАК Продажи\n{ГДЕ\n\tПродажи.Период >= &ДатаНачала\n\tИ Продажи.Период < &ДатаОкончания}", + "fields": [ + "Организация: CatalogRef.Организации @dimension", + "Номенклатура: CatalogRef.Номенклатура @dimension", + "Количество: decimal(15,3)", + "Сумма: decimal(15,2)" + ] + } + ], + "totalFields": [ + "Количество: Сумма", + "Сумма: Сумма" + ], + "parameters": [ + "Период: StandardPeriod = LastMonth", + { "name": "ДатаНачала", "type": "date", "expression": "&Период.ДатаНачала", "availableAsField": false }, + { "name": "ДатаОкончания", "type": "date", "expression": "&Период.ДатаОкончания", "availableAsField": false } + ], + "settingsVariants": [{ + "name": "Основной", + "presentation": "Продажи по организациям", + "settings": { + "selection": ["Номенклатура", "Количество", "Сумма", "Auto"], + "filter": [ + { "field": "Организация", "op": "=", "use": false, "userSettingID": "auto" } + ], + "order": ["Сумма desc", "Auto"], + "outputParameters": { + "Заголовок": "Анализ продаж", + "ВыводитьЗаголовок": "Output" + }, + "dataParameters": [ + { "parameter": "Период", "value": { "variant": "LastMonth" }, "userSettingID": "auto" } + ], + "structure": [ + { + "type": "group", + "groupBy": ["Организация"], + "selection": ["Auto"], + "order": ["Auto"], + "children": [ + { "type": "group", "selection": ["Auto"], "order": ["Auto"] } + ] + } + ] + } + }] +} +``` From b67f4b21e6578aea88c2ffc842104aaf7ebd9963 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 21:32:29 +0300 Subject: [PATCH 15/35] Add SKD group to README, create skd-guide.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README: add СКД row to skills table, specs section, directory tree - docs/skd-guide.md: usage guide with workflow, DSL examples, scenarios Co-Authored-By: Claude Opus 4.6 --- README.md | 12 +++- docs/skd-guide.md | 155 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 docs/skd-guide.md diff --git a/README.md b/README.md index a1adf052..9a63716d 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ | Табличный документ (MXL) | 4 навыка `/mxl-*` | Анализ, создание, компиляция макетов печатных форм | [Подробнее](docs/mxl-guide.md) | | Управляемые формы (Form) | 6 навыков `/form-*` | Создание, анализ, генерация, модификация, валидация управляемых форм | [Подробнее](docs/form-guide.md) | | Роли (Role) | 3 навыка `/role-*` | Анализ прав роли, создание из JSON DSL, валидация | [Подробнее](docs/role-guide.md) | +| Схема компоновки (СКД) | 3 навыка `/skd-*` | Анализ, генерация из JSON DSL, валидация схем компоновки данных | [Подробнее](docs/skd-guide.md) | | Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — | ## Требования @@ -41,6 +42,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 +72,16 @@ ├── role-info/ # Анализ прав роли ├── role-compile/ # Создание роли из JSON DSL ├── role-validate/ # Валидация роли +├── skd-info/ # Анализ схемы компоновки данных +├── skd-compile/ # Компиляция СКД из JSON DSL +├── 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 +89,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/skd-guide.md b/docs/skd-guide.md new file mode 100644 index 00000000..c5931957 --- /dev/null +++ b/docs/skd-guide.md @@ -0,0 +1,155 @@ +# Схема компоновки данных (СКД) + +Навыки группы `/skd-*` позволяют анализировать, создавать и проверять схемы компоновки данных 1С — XML-файлы DataCompositionSchema (Template.xml). + +## Навыки + +| Навык | Параметры | Описание | +|-------|-----------|----------| +| `/skd-info` | ` [-Mode] [-Name]` | Анализ структуры СКД: наборы, поля, параметры, ресурсы, варианты (10 режимов) | +| `/skd-compile` | ` ` | Генерация Template.xml из JSON DSL: наборы, поля, итоги, параметры, варианты | +| `/skd-validate` | ` [-MaxErrors 20]` | Валидация структурной корректности: ~30 проверок | + +## Рабочий цикл + +``` +Описание отчёта (текст) → JSON DSL → /skd-compile → Template.xml → /skd-validate + → /skd-info +``` + +1. Claude формирует JSON-определение СКД (shorthand-поля, параметры, итоги, варианты) +2. `/skd-compile` генерирует Template.xml с корректными namespace, типами, группировками +3. `/skd-validate` проверяет корректность сгенерированного XML +4. `/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": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"] +``` + +Формат: `Поле: Функция` или `Поле: Функция(выражение)`. + +### Параметры — shorthand + +```json +"parameters": [ + "Период: StandardPeriod = LastMonth", + "Организация: CatalogRef.Организации" +] +``` + +### Вычисляемые поля — shorthand + +```json +"calculatedFields": ["Итого = Количество * Цена"] +``` + +### Объектная форма + +Все секции поддерживают полную объектную форму для сложных случаев (title, appearance, role с выражениями, userSettingID и т.д.). Подробности — в [спецификации 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"], + "settingsVariants": [{ + "name": "Основной", + "settings": { + "selection": ["Номенклатура", "Количество", "Сумма", "Auto"], + "structure": [{ + "type": "group", "groupBy": ["Организация"], + "selection": ["Auto"], "order": ["Auto"], + "children": [{ "type": "group", "selection": ["Auto"], "order": ["Auto"] }] + }] + } + }] +} +``` + +И вызовет `/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`) From f4d60d67bfa23ade1b6d283e9e689ce7ccf53079 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Feb 2026 22:09:03 +0300 Subject: [PATCH 16/35] Add DSL v2 shorthand improvements to skd-compile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type synonyms (число/строка/булево/дата/СправочникСсылка, case-insensitive), @autoDates for auto-generating ДатаНачала/ДатаОкончания from StandardPeriod, string shorthand for structure ("Организация > details"), filter shorthand ("Организация = _ @off @user"), dataParameters shorthand ("Период = LastMonth @user"), and default selection/order ["Auto"] on all structure levels. Compression ratio improved from 3.9x to 5.8x on the medium example. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-compile/SKILL.md | 69 ++- .../skd-compile/scripts/skd-compile.ps1 | 434 +++++++++++++++--- docs/skd-dsl-spec.md | 150 ++++-- 3 files changed, 543 insertions(+), 110 deletions(-) diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md index 38a21d6d..161bc981 100644 --- a/.claude/skills/skd-compile/SKILL.md +++ b/.claude/skills/skd-compile/SKILL.md @@ -62,6 +62,8 @@ powershell.exe -NoProfile -File .claude\skills\skd-compile\scripts\skd-compile.p Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. +**Синонимы типов** (русские и альтернативные): `число` = 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`. @@ -72,26 +74,62 @@ powershell.exe -NoProfile -File .claude\skills\skd-compile\scripts\skd-compile.p "totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"] ``` -### Параметры (shorthand) +### Параметры (shorthand + @autoDates) ```json -"parameters": ["Период: StandardPeriod = LastMonth", "Организация: CatalogRef.Организации"] +"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`. + +### Параметры данных — shorthand + +```json +"dataParameters": [ + "Период = LastMonth @user", + "Организация @off @user" +] +``` + +Формат: `"Имя [= значение] @флаги"`. Для StandardPeriod варианты (LastMonth, ThisYear и т.д.) распознаются автоматически. + +### Структура — string shorthand + +```json +"structure": "Организация > details" +"structure": "Организация > Номенклатура > details" +``` + +`>` разделяет уровни группировки. `details` (или `детали`) = детальные записи. `selection` и `order` по умолчанию `["Auto"]` на каждом уровне. + +Для сложных случаев (таблицы, диаграммы, фильтры на уровне группировки) используется объектная форма. + ### Варианты настроек ```json "settingsVariants": [{ "name": "Основной", - "presentation": "Основной", "settings": { - "selection": ["Наименование", "Количество", "Auto"], - "filter": [{ "field": "Организация", "op": "=", "use": false, "userSettingID": "auto" }], + "selection": ["Номенклатура", "Количество", "Auto"], + "filter": ["Организация = _ @off @user"], "order": ["Количество desc", "Auto"], "outputParameters": { "Заголовок": "Мой отчёт" }, - "structure": [{ "type": "group", "groupBy": ["Организация"], "selection": ["Auto"], "order": ["Auto"], - "children": [{ "type": "group", "selection": ["Auto"], "order": ["Auto"] }] - }] + "dataParameters": ["Период = LastMonth @user"], + "structure": "Организация > details" } }] ``` @@ -109,16 +147,25 @@ powershell.exe -NoProfile -File .claude\skills\skd-compile\scripts\skd-compile.p } ``` -### С ресурсами и параметрами +### С ресурсами, параметрами и @autoDates ```json { "dataSets": [{ "query": "ВЫБРАТЬ Продажи.Номенклатура, Продажи.Количество, Продажи.Сумма ИЗ РегистрНакопления.Продажи КАК Продажи", - "fields": ["Номенклатура: CatalogRef.Номенклатура @dimension", "Количество: decimal(15,3)", "Сумма: decimal(15,2)"] + "fields": ["Номенклатура: СправочникСсылка.Номенклатура @dimension", "Количество: число(15,3)", "Сумма: число(15,2)"] }], "totalFields": ["Количество: Сумма", "Сумма: Сумма"], - "parameters": ["Период: StandardPeriod = LastMonth"] + "parameters": ["Период: СтандартныйПериод = LastMonth @autoDates"], + "settingsVariants": [{ + "name": "Основной", + "settings": { + "selection": ["Номенклатура", "Количество", "Сумма", "Auto"], + "filter": ["Организация = _ @off @user"], + "dataParameters": ["Период = LastMonth @user"], + "structure": "Организация > details" + } + }] } ``` diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 72fd6e0e..3e208fd5 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -80,11 +80,71 @@ foreach ($ds in $def.dataSets) { # --- 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" @@ -178,7 +238,7 @@ function Parse-FieldShorthand { if ($s.Contains(':')) { $parts = $s -split ':', 2 $result.dataPath = $parts[0].Trim() - $result.type = $parts[1].Trim() + $result.type = Resolve-TypeStr ($parts[1].Trim()) } else { $result.dataPath = $s } @@ -211,12 +271,18 @@ function Parse-TotalShorthand { function Parse-ParamShorthand { param([string]$s) - $result = @{ name = ""; type = ""; value = $null } + $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 = $Matches[2].Trim() + $result.type = Resolve-TypeStr ($Matches[2].Trim()) if ($Matches[4]) { $result.value = $Matches[4].Trim() } @@ -243,6 +309,128 @@ function Parse-CalcShorthand { 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', '' + } + + $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 = @{ @@ -293,7 +481,7 @@ function Emit-Field { dataPath = "$($fieldDef.dataPath)" field = if ($fieldDef.field) { "$($fieldDef.field)" } else { "$($fieldDef.dataPath)" } title = if ($fieldDef.title) { "$($fieldDef.title)" } else { "" } - type = if ($fieldDef.type) { "$($fieldDef.type)" } else { "" } + type = if ($fieldDef.type) { Resolve-TypeStr "$($fieldDef.type)" } else { "" } roles = @() restrict = @() appearance = @{} @@ -511,8 +699,9 @@ function Emit-CalcFields { Emit-MLText -tag "title" -text "$($cf.title)" -indent "`t`t" } if ($cf.type) { + $cfType = Resolve-TypeStr "$($cf.type)" X "`t`t" - Emit-ValueType -typeStr "$($cf.type)" -indent "`t`t`t" + Emit-ValueType -typeStr $cfType -indent "`t`t`t" X "`t`t" } if ($cf.restrict) { @@ -568,6 +757,52 @@ function Emit-TotalFields { } # === 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$($p.use)" + } + + X "`t" +} + function Emit-Parameters { if (-not $def.parameters) { return } foreach ($p in $def.parameters) { @@ -576,51 +811,31 @@ function Emit-Parameters { } else { $parsed = @{ name = "$($p.name)" - type = if ($p.type) { "$($p.type)" } else { "" } + 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 } } - X "`t" - X "`t`t$(Esc-Xml $parsed.name)" + Emit-SingleParam -p $p -parsed $parsed - # Title - $title = if ($p -isnot [string] -and $p.title) { "$($p.title)" } else { "" } - if ($title) { - Emit-MLText -tag "title" -text $title -indent "`t`t" + # @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 } - - # 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 ($p -isnot [string] -and $p.expression) { - X "`t`t$(Esc-Xml "$($p.expression)")" - } - - # AvailableAsField - if ($p -isnot [string] -and $p.availableAsField -eq $false) { - X "`t`tfalse" - } - - # Use - if ($p -isnot [string] -and $p.use) { - X "`t`t$($p.use)" - } - - X "`t" } } @@ -806,7 +1021,31 @@ function Emit-Filter { X "$indent" foreach ($item in $items) { - Emit-FilterItem -item $item -indent "$indent`t" + 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" } @@ -873,6 +1112,26 @@ function Emit-DataParameters { 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) { @@ -883,8 +1142,13 @@ function Emit-DataParameters { # Value if ($null -ne $dp.value) { - if ($dp.value.variant) { - # StandardPeriod + if ($dp.value -is [PSCustomObject] -and $dp.value.variant) { + # StandardPeriod (object form from JSON) + X "$indent`t`t" + X "$indent`t`t`t$($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$($dp.value.variant)" X "$indent`t`t" @@ -945,6 +1209,37 @@ function Emit-GroupItems { 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) @@ -958,8 +1253,17 @@ function Emit-StructureItem { } Emit-GroupItems -groupBy $item.groupBy -indent "$indent`t" - Emit-Order -items $item.order -indent "$indent`t" - Emit-Selection -items $item.selection -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) { @@ -987,8 +1291,10 @@ function Emit-StructureItem { foreach ($col in $item.columns) { X "$indent`t" Emit-GroupItems -groupBy $col.groupBy -indent "$indent`t`t" - Emit-Order -items $col.order -indent "$indent`t`t" - Emit-Selection -items $col.selection -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" } } @@ -1001,8 +1307,10 @@ function Emit-StructureItem { X "$indent`t`t$(Esc-Xml "$($row.name)")" } Emit-GroupItems -groupBy $row.groupBy -indent "$indent`t`t" - Emit-Order -items $row.order -indent "$indent`t`t" - Emit-Selection -items $row.selection -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" } } @@ -1020,8 +1328,10 @@ function Emit-StructureItem { if ($item.points) { X "$indent`t" Emit-GroupItems -groupBy $item.points.groupBy -indent "$indent`t`t" - Emit-Order -items $item.points.order -indent "$indent`t`t" - Emit-Selection -items $item.points.selection -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" } @@ -1029,8 +1339,10 @@ function Emit-StructureItem { if ($item.series) { X "$indent`t" Emit-GroupItems -groupBy $item.series.groupBy -indent "$indent`t`t" - Emit-Order -items $item.series.order -indent "$indent`t`t" - Emit-Selection -items $item.series.selection -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" } @@ -1120,9 +1432,13 @@ function Emit-SettingsVariants { Emit-DataParameters -items $s.dataParameters -indent "`t`t`t" } - # Structure + # Structure (supports string shorthand: "Организация > details") if ($s.structure) { - foreach ($item in $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" } } diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md index 52c3eb6c..19eb76ba 100644 --- a/docs/skd-dsl-spec.md +++ b/docs/skd-dsl-spec.md @@ -153,6 +153,27 @@ | `ChartOfAccountsRef.XXX` | `cfg:ChartOfAccountsRef.XXX` | — | | `StandardPeriod` | `v8:StandardPeriod` | — | +### Синонимы типов + +Все имена типов регистронезависимые. Поддерживаются русские и альтернативные имена: + +| Синоним | Канонический тип | +|---------|-----------------| +| `число`, `Число` | `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 | @@ -239,14 +260,14 @@ ### Shorthand ``` -": [= ]" +": [= ] [@autoDates]" ``` Примеры: ```json "parameters": [ - "Период: StandardPeriod = LastMonth", + "Период: StandardPeriod = LastMonth @autoDates", "Организация: CatalogRef.Организации", "ДатаОтчета: date" ] @@ -254,6 +275,26 @@ **Парсинг:** `"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 @@ -382,6 +423,27 @@ ### filter +#### Shorthand-строка + +```json +"filter": [ + "Организация = _ @off @user", + "Дата >= 2024-01-01T00:00:00", + "Статус filled", + "Количество > 0" +] +``` + +Формат: `"<Поле> <оператор> [<значение>] [@off] [@user] [@quickAccess]"`. + +- Значение `_` — пустое (placeholder, не выводится в XML) +- `@off` → `use=false` +- `@user` → `userSettingID=auto` (генерировать GUID) +- `@quickAccess` → `viewMode=QuickAccess` +- Типы значений автоопределяются: `true`/`false` → boolean, `2024-01-01T00:00:00` → dateTime, числа → decimal, прочее → string + +#### Объектная форма + ```json "filter": [ { "field": "Организация", "op": "=", "use": false, "userSettingID": "auto" }, @@ -458,6 +520,22 @@ ### dataParameters +#### Shorthand-строка + +```json +"dataParameters": [ + "Период = LastMonth @user", + "Организация @off @user" +] +``` + +Формат: `"<Имя> [= <значение>] [@off] [@user] [@quickAccess] [@normal]"`. + +- Значения-варианты периодов (`LastMonth`, `ThisYear` и др.) автоматически оборачиваются в `v8:StandardPeriod` +- `@off` → `use=false`, `@user` → `userSettingID=auto` + +#### Объектная форма + ```json "dataParameters": [ { "parameter": "Период", "value": { "variant": "LastMonth" }, "userSettingID": "auto" }, @@ -467,20 +545,32 @@ ### structure +#### String shorthand (рекомендуется для типичных случаев) + +```json +"structure": "Организация > details" +"structure": "Организация > Номенклатура > details" +"structure": "Период > Организация > Номенклатура > details" +``` + +`>` разделяет уровни вложенности. Каждый сегмент — группировка по указанному полю. `details` (или `детали`) — детальные записи (пустой `groupBy`). Для каждого уровня `selection` и `order` автоматически `["Auto"]`. + +#### Массив объектов + ```json "structure": [ { "type": "group", "groupBy": ["Организация"], - "selection": ["Auto"], - "order": ["Auto"], "children": [ - { "type": "group", "selection": ["Auto"], "order": ["Auto"] } + { "type": "group" } ] } ] ``` +**Умолчания:** `selection` и `order` по умолчанию `["Auto"]` на каждом уровне (в группировках, строках/колонках таблиц, точках/сериях диаграмм). Указывать явно нужно только если требуется другой набор полей. + #### Группировка (group) | Поле | Описание | @@ -489,9 +579,9 @@ | `name` | Имя группировки (опц.) | | `groupBy` | Массив полей. Пусто/опущено = детальные записи | | `groupType` | `"Items"` (умолч.), `"Hierarchy"`, `"HierarchyOnly"` | -| `selection` | Выборка (как в settings) | +| `selection` | Выборка (умолч. `["Auto"]`) | | `filter` | Отборы (как в settings) | -| `order` | Сортировка (как в settings) | +| `order` | Сортировка (умолч. `["Auto"]`) | | `outputParameters` | Параметры вывода (как в settings) | | `children` | Вложенные элементы структуры | @@ -570,18 +660,15 @@ "totalFields": ["Количество: Сумма"], "settingsVariants": [{ "name": "Основной", - "presentation": "Основной", "settings": { "selection": ["Наименование", "Количество"], - "structure": [ - { "type": "group", "order": ["Auto"], "selection": ["Auto"] } - ] + "structure": [{ "type": "group" }] } }] } ``` -## 12. Полный пример — средний +## 12. Полный пример — средний (с shorthand v2) ```json { @@ -590,50 +677,33 @@ "name": "Продажи", "query": "ВЫБРАТЬ\n\tПродажи.Организация,\n\tПродажи.Номенклатура,\n\tПродажи.Количество,\n\tПродажи.Сумма\nИЗ\n\tРегистрНакопления.Продажи КАК Продажи\n{ГДЕ\n\tПродажи.Период >= &ДатаНачала\n\tИ Продажи.Период < &ДатаОкончания}", "fields": [ - "Организация: CatalogRef.Организации @dimension", - "Номенклатура: CatalogRef.Номенклатура @dimension", - "Количество: decimal(15,3)", - "Сумма: decimal(15,2)" + "Организация: СправочникСсылка.Организации @dimension", + "Номенклатура: СправочникСсылка.Номенклатура @dimension", + "Количество: число(15,3)", + "Сумма: число(15,2)" ] } ], - "totalFields": [ - "Количество: Сумма", - "Сумма: Сумма" - ], + "totalFields": ["Количество: Сумма", "Сумма: Сумма"], "parameters": [ - "Период: StandardPeriod = LastMonth", - { "name": "ДатаНачала", "type": "date", "expression": "&Период.ДатаНачала", "availableAsField": false }, - { "name": "ДатаОкончания", "type": "date", "expression": "&Период.ДатаОкончания", "availableAsField": false } + "Период: СтандартныйПериод = LastMonth @autoDates" ], "settingsVariants": [{ "name": "Основной", "presentation": "Продажи по организациям", "settings": { "selection": ["Номенклатура", "Количество", "Сумма", "Auto"], - "filter": [ - { "field": "Организация", "op": "=", "use": false, "userSettingID": "auto" } - ], + "filter": ["Организация = _ @off @user"], "order": ["Сумма desc", "Auto"], "outputParameters": { "Заголовок": "Анализ продаж", "ВыводитьЗаголовок": "Output" }, - "dataParameters": [ - { "parameter": "Период", "value": { "variant": "LastMonth" }, "userSettingID": "auto" } - ], - "structure": [ - { - "type": "group", - "groupBy": ["Организация"], - "selection": ["Auto"], - "order": ["Auto"], - "children": [ - { "type": "group", "selection": ["Auto"], "order": ["Auto"] } - ] - } - ] + "dataParameters": ["Период = LastMonth @user"], + "structure": "Организация > details" } }] } ``` + +**Сравнение с v1:** средний пример сократился с 58 до 33 строк (−43%). Основная экономия: `@autoDates` (−4 строки), structure shorthand (−9 строк), filter/dataParam shorthand (−4 строки). From 91e4e1948fea97b2103cfe54ffbe03ee134f09de Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 09:44:23 +0300 Subject: [PATCH 17/35] Add comprehensive XML escaping to skd-compile Wrap all user-provided values in Esc-Xml() for safe XML output: - ValueType references (CatalogRef, DocumentRef, etc.) - Emit-ParamValue (all type branches: StandardPeriod, dateTime, boolean, decimal) - DataParameters (variant, boolean, dateTime values, viewMode, userSettingID) - Filter items (field, comparisonType, viewMode, userSettingID) - Selection fields, Order fields, GroupItems fields - GroupTemplates templateType, HorizontalAlign appearance value - Parameter use element Verified against 12,495 real DCS files: standard XML entity escaping (& < > ") matches 1C platform behavior exactly. Co-Authored-By: Claude Opus 4.6 --- .../skd-compile/scripts/skd-compile.ps1 | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 3e208fd5..d6597c74 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -197,15 +197,15 @@ function Emit-ValueType { # cfg: references (CatalogRef.XXX, DocumentRef.XXX, EnumRef.XXX, etc.) if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { - X "$indentcfg:$typeStr" + X "$indentcfg:$(Esc-Xml $typeStr)" return } # Fallback if ($typeStr.Contains('.')) { - X "$indentcfg:$typeStr" + X "$indentcfg:$(Esc-Xml $typeStr)" } else { - X "$indent$typeStr" + X "$indent$(Esc-Xml $typeStr)" } } @@ -465,7 +465,7 @@ function Emit-DataSources { foreach ($ds in $dataSources) { X "`t" X "`t`t$(Esc-Xml $ds.name)" - X "`t`t$($ds.type)" + X "`t`t$(Esc-Xml $ds.type)" X "`t" } } @@ -591,7 +591,7 @@ function Emit-Field { X "$indent`t`t" X "$indent`t`t`t$(Esc-Xml $key)" if ($key -eq "ГоризонтальноеПоложение") { - X "$indent`t`t`t$val" + X "$indent`t`t`t$(Esc-Xml $val)" } else { X "$indent`t`t`t$(Esc-Xml $val)" } @@ -797,7 +797,7 @@ function Emit-SingleParam { # Use if ($p -isnot [string] -and $p.use) { - X "`t`t$($p.use)" + X "`t`t$(Esc-Xml "$($p.use)")" } X "`t" @@ -849,22 +849,22 @@ function Emit-ParamValue { if ($type -eq "StandardPeriod") { # val is a period variant string like "LastMonth" X "$indent" - X "$indent`t$valStr" + X "$indent`t$(Esc-Xml $valStr)" X "$indent" } elseif ($type -match '^date') { - X "$indent$valStr" + X "$indent$(Esc-Xml $valStr)" } elseif ($type -eq "boolean") { - X "$indent$valStr" + X "$indent$(Esc-Xml $valStr)" } elseif ($type -match '^decimal') { - X "$indent$valStr" + 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$valStr" + X "$indent$(Esc-Xml $valStr)" } elseif ($valStr -eq "true" -or $valStr -eq "false") { - X "$indent$valStr" + X "$indent$(Esc-Xml $valStr)" } else { X "$indent$(Esc-Xml $valStr)" } @@ -899,7 +899,7 @@ function Emit-GroupTemplates { foreach ($gt in $def.groupTemplates) { X "`t" X "`t`t$(Esc-Xml "$($gt.groupField)")" - X "`t`t$($gt.templateType)" + X "`t`t$(Esc-Xml "$($gt.templateType)")" X "`t`t" X "`t" } @@ -919,12 +919,12 @@ function Emit-Selection { X "$indent`t" } else { X "$indent`t" - X "$indent`t`t$item" + X "$indent`t`t$(Esc-Xml $item)" X "$indent`t" } } else { X "$indent`t" - X "$indent`t`t$($item.field)" + X "$indent`t`t$(Esc-Xml "$($item.field)")" if ($item.title) { X "$indent`t`t" X "$indent`t`t`t" @@ -968,11 +968,11 @@ function Emit-FilterItem { X "$indent`tfalse" } - X "$indent`t$($item.field)" + X "$indent`t$(Esc-Xml "$($item.field)")" $compType = $script:comparisonTypes["$($item.op)"] if (-not $compType) { $compType = "$($item.op)" } - X "$indent`t$compType" + X "$indent`t$(Esc-Xml $compType)" # Right value if ($null -ne $item.value) { @@ -1003,12 +1003,12 @@ function Emit-FilterItem { } if ($item.viewMode) { - X "$indent`t$($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$uid" + X "$indent`t$(Esc-Xml $uid)" } X "$indent" @@ -1067,7 +1067,7 @@ function Emit-Order { 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$field" + X "$indent`t`t$(Esc-Xml $field)" X "$indent`t`t$dir" X "$indent`t" } @@ -1145,30 +1145,30 @@ function Emit-DataParameters { if ($dp.value -is [PSCustomObject] -and $dp.value.variant) { # StandardPeriod (object form from JSON) X "$indent`t`t" - X "$indent`t`t`t$($dp.value.variant)" + 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$($dp.value.variant)" + 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$bv" + X "$indent`t`t$(Esc-Xml $bv)" } elseif ("$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') { - X "$indent`t`t$($dp.value)" + X "$indent`t`t$(Esc-Xml "$($dp.value)")" } else { X "$indent`t`t$(Esc-Xml "$($dp.value)")" } } if ($dp.viewMode) { - X "$indent`t`t$($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$uid" + X "$indent`t`t$(Esc-Xml $uid)" } X "$indent`t" @@ -1187,7 +1187,7 @@ function Emit-GroupItems { foreach ($field in $groupBy) { if ($field -is [string]) { X "$indent`t" - X "$indent`t`t$field" + X "$indent`t`t$(Esc-Xml $field)" X "$indent`t`tItems" X "$indent`t`tNone" X "$indent`t`t0001-01-01T00:00:00" @@ -1196,11 +1196,11 @@ function Emit-GroupItems { } else { # Object form X "$indent`t" - X "$indent`t`t$($field.field)" + X "$indent`t`t$(Esc-Xml "$($field.field)")" $gt = if ($field.groupType) { "$($field.groupType)" } else { "Items" } - X "$indent`t`t$gt" + X "$indent`t`t$(Esc-Xml $gt)" $pat = if ($field.periodAdditionType) { "$($field.periodAdditionType)" } else { "None" } - X "$indent`t`t$pat" + 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" From eb6f8379e699637bb0b77db587fee93222529fa2 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 11:32:17 +0300 Subject: [PATCH 18/35] Skip cfg: reference types in DCS valueType to fix XDTO build errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real 1C DCS files (12,495 analyzed) never include cfg:CatalogRef.XXX in — the platform infers field types from query metadata. Emitting them causes XDTO exceptions when building EPF. Reference types in JSON DSL still set field roles but no longer emit valueType. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-compile/SKILL.md | 2 +- .../skd-compile/scripts/skd-compile.ps1 | 23 ++++++++++++------- docs/skd-dsl-spec.md | 10 ++++---- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md index 161bc981..e0bfdc06 100644 --- a/.claude/skills/skd-compile/SKILL.md +++ b/.claude/skills/skd-compile/SKILL.md @@ -60,7 +60,7 @@ powershell.exe -NoProfile -File .claude\skills\skd-compile\scripts\skd-compile.p "Служебное: string #noFilter #noOrder" — + ограничения ``` -Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. +Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы (`CatalogRef`, `DocumentRef`, `EnumRef`) задают роли полей, но не эмитируются в `` — платформа определяет их из запроса. **Синонимы типов** (русские и альтернативные): `число` = decimal, `строка` = string, `булево` = boolean, `дата` = date, `датаВремя` = dateTime, `СтандартныйПериод` = StandardPeriod, `СправочникСсылка.X` = CatalogRef.X, `ДокументСсылка.X` = DocumentRef.X, `int`/`number` = decimal, `bool` = boolean. Регистронезависимые. diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index d6597c74..d47d13f3 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -195,18 +195,25 @@ function Emit-ValueType { return } - # cfg: references (CatalogRef.XXX, DocumentRef.XXX, EnumRef.XXX, etc.) + # cfg: references — skip in DCS (platform infers types from query metadata) + # Emitting cfg:CatalogRef.XXX causes XDTO errors; real DCS files never use them if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { - X "$indentcfg:$(Esc-Xml $typeStr)" return } - # Fallback + # Fallback — skip dot-qualified types (likely cfg: references) if ($typeStr.Contains('.')) { - X "$indentcfg:$(Esc-Xml $typeStr)" - } else { - X "$indent$(Esc-Xml $typeStr)" + return } + + X "$indent$(Esc-Xml $typeStr)" +} + +function Is-RefType { + param([string]$typeStr) + if (-not $typeStr) { return $false } + $resolved = Resolve-TypeStr $typeStr + return ($resolved -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.' -or ($resolved.Contains('.') -and $resolved -notmatch '^(string|decimal)')) } # --- 5. Field shorthand parser --- @@ -576,8 +583,8 @@ function Emit-Field { X "$indent`t" } - # ValueType - if ($f.type) { + # ValueType (skip for cfg: reference types — platform infers from query) + if ($f.type -and -not (Is-RefType $f.type)) { X "$indent`t" Emit-ValueType -typeStr $f.type -indent "$indent`t`t" X "$indent`t" diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md index 19eb76ba..ba935303 100644 --- a/docs/skd-dsl-spec.md +++ b/docs/skd-dsl-spec.md @@ -147,12 +147,14 @@ | `boolean` | `xs:boolean` | — | | `date` | `xs:dateTime` | DateFractions=Date | | `dateTime` | `xs:dateTime` | DateFractions=DateTime | -| `CatalogRef.XXX` | `cfg:CatalogRef.XXX` | — | -| `DocumentRef.XXX` | `cfg:DocumentRef.XXX` | — | -| `EnumRef.XXX` | `cfg:EnumRef.XXX` | — | -| `ChartOfAccountsRef.XXX` | `cfg:ChartOfAccountsRef.XXX` | — | +| `CatalogRef.XXX` | *(не эмитируется)* | Роль поля сохраняется, `` опускается | +| `DocumentRef.XXX` | *(не эмитируется)* | Платформа определяет тип из запроса | +| `EnumRef.XXX` | *(не эмитируется)* | | +| `ChartOfAccountsRef.XXX` | *(не эмитируется)* | | | `StandardPeriod` | `v8:StandardPeriod` | — | +> **Ссылочные типы** (`CatalogRef.XXX`, `DocumentRef.XXX` и др.) указываются в JSON для задания ролей полей (`@dimension`, `@account` и т.д.), но **не эмитируются** в ``. Реальные DCS-файлы 1С не содержат `cfg:` ссылок в valueType — платформа определяет типы полей автоматически из метаданных запроса. Включение `cfg:` типов приводит к ошибке XDTO при загрузке. + ### Синонимы типов Все имена типов регистронезависимые. Поддерживаются русские и альтернативные имена: From f23a3e55369f364cef8259c407fa3b0f14358d03 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 11:35:47 +0300 Subject: [PATCH 19/35] Add MIT license Co-Authored-By: Claude Opus 4.6 --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE 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. From 2650b1a06300bd275e26e6db90dab41d03ca86f7 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 11:45:01 +0300 Subject: [PATCH 20/35] Fix reference types to use d5p1: namespace instead of cfg: Real DCS files use inline xmlns:d5p1="http://v8.1c.ru/8.1/data/ enterprise/current-config" for reference types like CatalogRef.XXX. Using cfg: prefix caused XDTO errors because the namespace was undeclared. With correct d5p1: namespace, EPF builds successfully in both empty and config-aware bases. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-compile/SKILL.md | 2 +- .../skd-compile/scripts/skd-compile.ps1 | 19 +++++++------------ docs/skd-dsl-spec.md | 10 +++++----- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md index e0bfdc06..93fc6350 100644 --- a/.claude/skills/skd-compile/SKILL.md +++ b/.claude/skills/skd-compile/SKILL.md @@ -60,7 +60,7 @@ powershell.exe -NoProfile -File .claude\skills\skd-compile\scripts\skd-compile.p "Служебное: string #noFilter #noOrder" — + ограничения ``` -Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы (`CatalogRef`, `DocumentRef`, `EnumRef`) задают роли полей, но не эмитируются в `` — платформа определяет их из запроса. +Типы: `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. Регистронезависимые. diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index d47d13f3..0f94a40d 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -195,27 +195,22 @@ function Emit-ValueType { return } - # cfg: references — skip in DCS (platform infers types from query metadata) - # Emitting cfg:CatalogRef.XXX causes XDTO errors; real DCS files never use them + # 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 — skip dot-qualified types (likely cfg: references) + # Fallback — assume dot-qualified types are also config references if ($typeStr.Contains('.')) { + X "$indentd5p1:$(Esc-Xml $typeStr)" return } X "$indent$(Esc-Xml $typeStr)" } -function Is-RefType { - param([string]$typeStr) - if (-not $typeStr) { return $false } - $resolved = Resolve-TypeStr $typeStr - return ($resolved -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.' -or ($resolved.Contains('.') -and $resolved -notmatch '^(string|decimal)')) -} - # --- 5. Field shorthand parser --- function Parse-FieldShorthand { @@ -583,8 +578,8 @@ function Emit-Field { X "$indent`t" } - # ValueType (skip for cfg: reference types — platform infers from query) - if ($f.type -and -not (Is-RefType $f.type)) { + # ValueType + if ($f.type) { X "$indent`t" Emit-ValueType -typeStr $f.type -indent "$indent`t`t" X "$indent`t" diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md index ba935303..1c96ceef 100644 --- a/docs/skd-dsl-spec.md +++ b/docs/skd-dsl-spec.md @@ -147,13 +147,13 @@ | `boolean` | `xs:boolean` | — | | `date` | `xs:dateTime` | DateFractions=Date | | `dateTime` | `xs:dateTime` | DateFractions=DateTime | -| `CatalogRef.XXX` | *(не эмитируется)* | Роль поля сохраняется, `` опускается | -| `DocumentRef.XXX` | *(не эмитируется)* | Платформа определяет тип из запроса | -| `EnumRef.XXX` | *(не эмитируется)* | | -| `ChartOfAccountsRef.XXX` | *(не эмитируется)* | | +| `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` и др.) указываются в JSON для задания ролей полей (`@dimension`, `@account` и т.д.), но **не эмитируются** в ``. Реальные DCS-файлы 1С не содержат `cfg:` ссылок в valueType — платформа определяет типы полей автоматически из метаданных запроса. Включение `cfg:` типов приводит к ошибке XDTO при загрузке. +> **Ссылочные типы** (`CatalogRef.XXX`, `DocumentRef.XXX` и др.) эмитируются с inline namespace declaration: `d5p1:CatalogRef.XXX`. Использование префикса `cfg:` вместо `d5p1:` с объявлением namespace приводит к ошибке XDTO. Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией (не пустую). ### Синонимы типов From 4594c74d212cad6ae7beb0fd966284e24c91827b Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 11:47:04 +0300 Subject: [PATCH 21/35] Document d5p1: inline namespace for reference types in DCS spec Reference types in valueType use inline xmlns:d5p1 declaration with URI http://v8.1c.ru/8.1/data/enterprise/current-config, not a root-level cfg: prefix. Added XML example and supported type list. Co-Authored-By: Claude Opus 4.6 --- docs/1c-dcs-spec.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/1c-dcs-spec.md b/docs/1c-dcs-spec.md index c97b7242..3eac72a1 100644 --- a/docs/1c-dcs-spec.md +++ b/docs/1c-dcs-spec.md @@ -313,7 +313,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) @@ -515,7 +523,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` и др. From 7e587b9a3e7ac2ab5bd0d92d51dd1d24a22d8fbc Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 12:48:52 +0300 Subject: [PATCH 22/35] Emit Auto items only at group level, not top-level settings Real DCS files place SelectedItemAuto and OrderItemAuto exclusively inside StructureItem elements (groups, tables, charts), never at the top-level settings. Added -skipAuto switch to Emit-Selection and Emit-Order, applied at settings level. Co-Authored-By: Claude Opus 4.6 --- .../skd-compile/scripts/skd-compile.ps1 | 20 +++++++++++-------- docs/skd-dsl-spec.md | 4 ++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 0f94a40d..f6828a3d 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -910,7 +910,7 @@ function Emit-GroupTemplates { # === Settings Variants === function Emit-Selection { - param($items, [string]$indent) + param($items, [string]$indent, [switch]$skipAuto) if (-not $items -or $items.Count -eq 0) { return } @@ -918,7 +918,9 @@ function Emit-Selection { foreach ($item in $items) { if ($item -is [string]) { if ($item -eq "Auto") { - X "$indent`t" + if (-not $skipAuto) { + X "$indent`t" + } } else { X "$indent`t" X "$indent`t`t$(Esc-Xml $item)" @@ -1053,7 +1055,7 @@ function Emit-Filter { } function Emit-Order { - param($items, [string]$indent) + param($items, [string]$indent, [switch]$skipAuto) if (-not $items -or $items.Count -eq 0) { return } @@ -1061,7 +1063,9 @@ function Emit-Order { foreach ($item in $items) { if ($item -is [string]) { if ($item -eq "Auto") { - X "$indent`t" + if (-not $skipAuto) { + X "$indent`t" + } } else { $parts = $item -split '\s+' $field = $parts[0] @@ -1409,9 +1413,9 @@ function Emit-SettingsVariants { $s = $v.settings - # Selection + # Selection (Auto items only belong at group level, not top-level settings) if ($s.selection) { - Emit-Selection -items $s.selection -indent "`t`t`t" + Emit-Selection -items $s.selection -indent "`t`t`t" -skipAuto } # Filter @@ -1419,9 +1423,9 @@ function Emit-SettingsVariants { Emit-Filter -items $s.filter -indent "`t`t`t" } - # Order + # Order (Auto items only belong at group level, not top-level settings) if ($s.order) { - Emit-Order -items $s.order -indent "`t`t`t" + Emit-Order -items $s.order -indent "`t`t`t" -skipAuto } # OutputParameters diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md index 1c96ceef..50c57661 100644 --- a/docs/skd-dsl-spec.md +++ b/docs/skd-dsl-spec.md @@ -420,7 +420,7 @@ ``` - Строка → `SelectedItemField` -- `"Auto"` → `SelectedItemAuto` +- `"Auto"` → `SelectedItemAuto` (только на уровне группировок; на верхнем уровне settings игнорируется) - Объект с `field`/`title` → `SelectedItemField` с `lwsTitle` ### filter @@ -498,7 +498,7 @@ - `"Field"` → `OrderItemField`, `orderType=Asc` - `"Field desc"` → `OrderItemField`, `orderType=Desc` - `"Field asc"` → `OrderItemField`, `orderType=Asc` -- `"Auto"` → `OrderItemAuto` +- `"Auto"` → `OrderItemAuto` (только на уровне группировок; на верхнем уровне settings игнорируется) ### outputParameters From 138219420104ba2fe50c48ad26a2cc204c180703 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 13:22:35 +0300 Subject: [PATCH 23/35] Fix @period role to emit periodNumber/periodType instead of period Real DCS files use 1 and Main for period fields. The element does not exist in the XDTO schema and causes build failures. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-compile/scripts/skd-compile.ps1 | 8 +++++++- docs/1c-dcs-spec.md | 3 ++- docs/skd-dsl-spec.md | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index f6828a3d..8ba7a00d 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -564,7 +564,13 @@ function Emit-Field { if ($f.roles.Count -gt 0 -or $f["roleObj"]) { X "$indent`t" foreach ($role in $f.roles) { - X "$indent`t`ttrue" + 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"] diff --git a/docs/1c-dcs-spec.md b/docs/1c-dcs-spec.md index 3eac72a1..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) diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md index 50c57661..71899815 100644 --- a/docs/skd-dsl-spec.md +++ b/docs/skd-dsl-spec.md @@ -183,7 +183,7 @@ | `@dimension` | `"role": "dimension"` или `{"dimension": true}` | `true` | | `@account` | `"role": "account"` или `{"account": true}` | `true` | | `@balance` | `"role": "balance"` или `{"balance": true}` | `true` | -| `@period` | `"role": "period"` или `{"period": true}` | `true` | +| `@period` | `"role": "period"` или `{"period": true}` | `1` + `Main` | Объектная форма с доп. полями: ```json From 91d254b7182562e4a2f54b6de1d7b78b3203d004 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 16:12:47 +0300 Subject: [PATCH 24/35] Add DCS template type, conditional appearance, filter/total enhancements - epf-add-template: support DataCompositionSchema template type with minimal DCS scaffold (dataSource + Local) - skd-compile: add conditionalAppearance with auto-detect value types (Color, LocalStringType, Boolean), selection, filter, presentation - skd-compile: add @normal, @inaccessible viewMode shorthand flags - skd-compile: add userSettingPresentation for filters and dataParameters - skd-compile: support multiple group elements in totalField (array form) - skd-compile SKILL.md: document all new features with examples Co-Authored-By: Claude Opus 4.6 --- .claude/skills/epf-add-template/SKILL.md | 3 +- .../epf-add-template/scripts/add-template.ps1 | 22 ++- .claude/skills/skd-compile/SKILL.md | 49 ++++++- .../skd-compile/scripts/skd-compile.ps1 | 127 +++++++++++++++++- 4 files changed, 195 insertions(+), 6 deletions(-) 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..456d3444 100644 --- a/.claude/skills/epf-add-template/scripts/add-template.ps1 +++ b/.claude/skills/epf-add-template/scripts/add-template.ps1 @@ -6,7 +6,7 @@ [string]$TemplateName, [Parameter(Mandatory)] - [ValidateSet("HTML", "Text", "SpreadsheetDocument", "BinaryData")] + [ValidateSet("HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema")] [string]$TemplateType, [string]$Synonym = $TemplateName, @@ -23,6 +23,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 +112,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/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md index 93fc6350..d4d7f191 100644 --- a/.claude/skills/skd-compile/SKILL.md +++ b/.claude/skills/skd-compile/SKILL.md @@ -94,7 +94,20 @@ powershell.exe -NoProfile -File .claude\skills\skd-compile\scripts\skd-compile.p ] ``` -Формат: `"Поле оператор значение @флаги"`. Значение `_` = пустое (placeholder). Флаги: `@off` (use=false), `@user` (userSettingID=auto), `@quickAccess`. +Формат: `"Поле оператор значение @флаги"`. Значение `_` = пустое (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 @@ -127,6 +140,15 @@ powershell.exe -NoProfile -File .claude\skills\skd-compile\scripts\skd-compile.p "selection": ["Номенклатура", "Количество", "Auto"], "filter": ["Организация = _ @off @user"], "order": ["Количество desc", "Auto"], + "conditionalAppearance": [ + { + "filter": ["Просрочено = true"], + "appearance": { "ЦветТекста": "style:ПросроченныеДанныеЦвет" }, + "presentation": "Выделять просроченные", + "viewMode": "Normal", + "userSettingID": "auto" + } + ], "outputParameters": { "Заголовок": "Мой отчёт" }, "dataParameters": ["Период = LastMonth @user"], "structure": "Организация > details" @@ -134,6 +156,31 @@ powershell.exe -NoProfile -File .claude\skills\skd-compile\scripts\skd-compile.p }] ``` +### Условное оформление (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 Иерархия", "ОбщийИтог"] } +] +``` + ## Примеры ### Минимальный diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 8ba7a00d..934986d2 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -381,6 +381,14 @@ function Parse-FilterShorthand { $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() @@ -751,14 +759,16 @@ function Emit-TotalFields { dataPath = "$($tf.dataPath)" expression = "$($tf.expression)" } - if ($tf.group) { $parsed.group = "$($tf.group)" } + 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.group) { - X "`t`t$(Esc-Xml $parsed.group)" + if ($parsed.groups) { + foreach ($g in $parsed.groups) { + X "`t`t$(Esc-Xml "$g")" + } } X "`t" } @@ -1021,6 +1031,15 @@ function Emit-FilterItem { 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" } @@ -1088,6 +1107,94 @@ function Emit-Order { 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) @@ -1183,6 +1290,15 @@ function Emit-DataParameters { 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" @@ -1434,6 +1550,11 @@ function Emit-SettingsVariants { 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" From 70bb5074c8be3354acdea23fcdb85331fba88148 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 16:19:31 +0300 Subject: [PATCH 25/35] Update SKD DSL spec and guide with new features documentation Add conditionalAppearance, multi-group totalField, viewMode flags (@normal, @inaccessible), userSettingPresentation, dataParameters table. Update guide with @autoDates, structure/filter shorthand, and modern examples. Co-Authored-By: Claude Opus 4.6 --- docs/skd-dsl-spec.md | 85 ++++++++++++++++++++++++++++++++++++++++++-- docs/skd-guide.md | 42 ++++++++++++++++------ 2 files changed, 113 insertions(+), 14 deletions(-) diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md index 71899815..9a970d1d 100644 --- a/docs/skd-dsl-spec.md +++ b/docs/skd-dsl-spec.md @@ -255,6 +255,27 @@ { "dataPath": "X", "expression": "Максимум(X)", "group": "Группа1" } ``` +### Привязка к группировкам (group) + +В объектной форме поле `group` может быть строкой или массивом строк. Каждая строка задаёт имя группировки, для которой вычисляется итог: + +```json +"totalFields": [ + { "dataPath": "Кол", "expression": "Сумма(Кол)", "group": ["ГруппаПользователей", "ГруппаПользователей Иерархия", "ОбщийИтог"] } +] +``` + +XML-маппинг — по `` на каждый элемент: +```xml + + Кол + Сумма(Кол) + ГруппаПользователей + ГруппаПользователей Иерархия + ОбщийИтог + +``` + --- ## 6. Параметры (parameters) @@ -402,6 +423,7 @@ "selection": [...], "filter": [...], "order": [...], + "conditionalAppearance": [...], "outputParameters": {...}, "dataParameters": [...], "structure": [...] @@ -436,12 +458,14 @@ ] ``` -Формат: `"<Поле> <оператор> [<значение>] [@off] [@user] [@quickAccess]"`. +Формат: `"<Поле> <оператор> [<значение>] [@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 #### Объектная форма @@ -467,6 +491,7 @@ | `presentation` | Текст подсказки | | `viewMode` | `"Normal"`, `"QuickAccess"`, `"Inaccessible"` | | `userSettingID` | `"auto"` → генерировать GUID | +| `userSettingPresentation` | Отображаемое имя настройки (LocalStringType) | Операторы: @@ -500,6 +525,51 @@ - `"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 @@ -531,7 +601,7 @@ ] ``` -Формат: `"<Имя> [= <значение>] [@off] [@user] [@quickAccess] [@normal]"`. +Формат: `"<Имя> [= <значение>] [@off] [@user] [@quickAccess] [@normal] [@inaccessible]"`. - Значения-варианты периодов (`LastMonth`, `ThisYear` и др.) автоматически оборачиваются в `v8:StandardPeriod` - `@off` → `use=false`, `@user` → `userSettingID=auto` @@ -541,10 +611,19 @@ ```json "dataParameters": [ { "parameter": "Период", "value": { "variant": "LastMonth" }, "userSettingID": "auto" }, - { "parameter": "Организация", "use": false, "viewMode": "Normal", "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 (рекомендуется для типичных случаев) diff --git a/docs/skd-guide.md b/docs/skd-guide.md index c5931957..98eaeb67 100644 --- a/docs/skd-guide.md +++ b/docs/skd-guide.md @@ -58,26 +58,49 @@ "totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"] ``` -Формат: `Поле: Функция` или `Поле: Функция(выражение)`. +Формат: `Поле: Функция` или `Поле: Функция(выражение)`. Объектная форма поддерживает привязку к группировкам: `{ "dataPath": "X", "expression": "Сумма(X)", "group": ["Группа1", "ОбщийИтог"] }`. -### Параметры — shorthand +### Параметры — shorthand + @autoDates ```json "parameters": [ - "Период: StandardPeriod = LastMonth", + "Период: 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 и т.д.). Подробности — в [спецификации SKD DSL](skd-dsl-spec.md). +Все секции поддерживают полную объектную форму для сложных случаев (title, appearance, role с выражениями, userSettingID, userSettingPresentation, conditionalAppearance, группы фильтров And/Or/Not и т.д.). Подробности — в [спецификации SKD DSL](skd-dsl-spec.md). ## Сценарии использования @@ -114,16 +137,13 @@ Claude сформирует JSON: ] }], "totalFields": ["Количество: Сумма", "Сумма: Сумма"], - "parameters": ["Период: StandardPeriod = LastMonth"], + "parameters": ["Период: StandardPeriod = LastMonth @autoDates"], "settingsVariants": [{ "name": "Основной", "settings": { - "selection": ["Номенклатура", "Количество", "Сумма", "Auto"], - "structure": [{ - "type": "group", "groupBy": ["Организация"], - "selection": ["Auto"], "order": ["Auto"], - "children": [{ "type": "group", "selection": ["Auto"], "order": ["Auto"] }] - }] + "selection": ["Номенклатура", "Количество", "Сумма"], + "dataParameters": ["Период = LastMonth @user"], + "structure": "Организация > details" } }] } From 151d1d77cbaf5e5174eb8eb4090a7e09a1c4c4b5 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 16:56:02 +0300 Subject: [PATCH 26/35] Add /skd-edit skill for atomic DCS modifications New skill for point editing of existing Template.xml: add-field, add-total, add-calculated-field, add-parameter (with @autoDates), add-filter, set-query. Uses XmlDocument+PreserveWhitespace pattern from form-edit and shorthand parsers from skd-compile. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-edit/SKILL.md | 110 +++ .claude/skills/skd-edit/scripts/skd-edit.ps1 | 947 +++++++++++++++++++ 2 files changed, 1057 insertions(+) create mode 100644 .claude/skills/skd-edit/SKILL.md create mode 100644 .claude/skills/skd-edit/scripts/skd-edit.ps1 diff --git a/.claude/skills/skd-edit/SKILL.md b/.claude/skills/skd-edit/SKILL.md new file mode 100644 index 00000000..8fd815e5 --- /dev/null +++ b/.claude/skills/skd-edit/SKILL.md @@ -0,0 +1,110 @@ +--- +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` | Операция: `add-field`, `add-total`, `add-calculated-field`, `add-parameter`, `add-filter`, `set-query` | +| `Value` | Значение операции (shorthand-строка или текст запроса) | +| `DataSet` | (опц.) Имя набора данных (умолч. первый) | +| `Variant` | (опц.) Имя варианта настроек (умолч. первый) | +| `NoSelection` | (опц.) Не добавлять поле в selection варианта | + +```powershell +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 -TemplatePath "" -Operation -Value "" +``` + +## Операции + +### add-field — добавить поле в набор данных + +Shorthand-формат из skd-compile: `"Цена: decimal(15,2)"`, `"Организация: CatalogRef.Организации @dimension"`. + +```powershell +-Operation add-field -Value "Цена: decimal(15,2)" +-Operation add-field -Value "Организация: CatalogRef.Организации @dimension" +-Operation add-field -Value "Служебное: string #noFilter #noOrder" +``` + +Поле добавляется перед `` в наборе, а также в `` первого варианта (если нет `-NoSelection`). + +### add-total — добавить итог + +```powershell +-Operation add-total -Value "Цена: Среднее" +-Operation add-total -Value "Стоимость: Сумма(Кол * Цена)" +``` + +### add-calculated-field — добавить вычисляемое поле + +```powershell +-Operation add-calculated-field -Value "Маржа = Продажа - Закупка" +``` + +Также добавляется в selection варианта (если нет `-NoSelection`). + +### add-parameter — добавить параметр + +```powershell +-Operation add-parameter -Value "Период: StandardPeriod = LastMonth @autoDates" +-Operation add-parameter -Value "Организация: CatalogRef.Организации" +``` + +`@autoDates` генерирует дополнительные параметры `ДатаНачала` и `ДатаОкончания`. + +### add-filter — добавить фильтр в вариант настроек + +```powershell +-Operation add-filter -Value "Номенклатура = _ @off @user" +-Operation add-filter -Value "Дата >= 2024-01-01T00:00:00" +-Operation add-filter -Value "Статус filled" +``` + +Формат: `"Поле оператор значение @флаги"`. Флаги: `@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`. + +### set-query — заменить текст запроса + +```powershell +-Operation set-query -Value "ВЫБРАТЬ 1 КАК Тест" +``` + +## Примеры + +```powershell +# Добавить числовое поле +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation add-field -Value "Цена: decimal(15,2)" + +# Добавить итог +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation add-total -Value "Цена: Среднее" + +# Добавить фильтр +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation add-filter -Value "Организация = _ @off @user" + +# Заменить запрос +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation set-query -Value "ВЫБРАТЬ 1 КАК Тест" +``` + +## Верификация + +``` +/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..fcbc811c --- /dev/null +++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1 @@ -0,0 +1,947 @@ +param( + [Parameter(Mandatory)] + [string]$TemplatePath, + + [Parameter(Mandatory)] + [ValidateSet("add-field","add-total","add-calculated-field", + "add-parameter","add-filter","set-query")] + [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" + +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 (copied from skd-compile) --- + +function Parse-FieldShorthand { + param([string]$s) + + $result = @{ + dataPath = ""; field = ""; title = ""; type = "" + roles = @(); restrict = @() + } + + $roleMatches = [regex]::Matches($s, '@(\w+)') + foreach ($m in $roleMatches) { + $result.roles += $m.Groups[1].Value + } + $s = [regex]::Replace($s, '\s*@\w+', '') + + $restrictMatches = [regex]::Matches($s, '#(\w+)') + foreach ($m in $restrictMatches) { + $result.restrict += $m.Groups[1].Value + } + $s = [regex]::Replace($s, '\s*#\w+', '') + + $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 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) + + $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 = "" } +} + +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 +} + +# --- 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-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" + } + } + $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.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)" + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-ParamFragment { + param($parsed, [string]$indent) + + $i = $indent + $fragments = @() + + # Main parameter + $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") + + # @autoDates: generate ДатаНачала and ДатаОкончания + 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 = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $fieldName)" + $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 { + # Append at end: insert before trailing whitespace + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $ws) | 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 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 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 + } + + # First dataset + 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 + + # Find settingsVariant + $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 + } + } + + # Find settings element inside variant + 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 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 + + $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) { + return $Variant + } + } + } + return $Variant + } else { + 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)" +} + +# --- 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. Main logic --- + +switch ($Operation) { + "add-field" { + $parsed = Parse-FieldShorthand $Value + $dsNode = Resolve-DataSet + $dsName = Get-DataSetName $dsNode + $childIndent = Get-ChildIndent $dsNode + + $fragXml = Build-FieldFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + # Insert before in dataset + $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`"" + + # Add to selection of variant + if (-not $NoSelection) { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selNs = $setNs + $selection = Find-FirstElement $settings @("selection") $selNs + if ($selection) { + $selIndent = Get-ChildIndent $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" { + $parsed = Parse-TotalShorthand $Value + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + + $fragXml = Build-TotalFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + # Insert after last totalField, or before parameter/template/groupTemplate/settingsVariant + $root = $xmlDoc.DocumentElement + $lastTotal = Find-LastElement $root "totalField" $schNs + if ($lastTotal) { + # Insert after last totalField = before next sibling + $refNode = $lastTotal.NextSibling + # Skip whitespace nodes + 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" { + $parsed = Parse-CalcShorthand $Value + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + + $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" + + # Add to selection + if (-not $NoSelection) { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selection = Find-FirstElement $settings @("selection") $setNs + if ($selection) { + $selIndent = Get-ChildIndent $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" { + $parsed = Parse-ParamShorthand $Value + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + + $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" { + $parsed = Parse-FilterShorthand $Value + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + # Find or create + $filterEl = Find-FirstElement $settings @("filter") $setNs + if (-not $filterEl) { + # Create in settings + $settingsIndent = Get-ChildIndent $settings + $filterFragXml = "$settingsIndent" + $filterNodes = Import-Fragment $xmlDoc $filterFragXml + # Insert after selection or as first child + $refNode = Find-FirstElement $settings @("selection") $setNs + if ($refNode) { + $refNode = $refNode.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } + foreach ($node in $filterNodes) { + Insert-BeforeElement $settings $node $refNode $settingsIndent + } + $filterEl = Find-FirstElement $settings @("filter") $setNs + } + + $filterIndent = Get-ChildIndent $filterEl + # If filter is self-closing (empty), we need a different indent + if (-not $filterEl.HasChildNodes) { + # Determine indent from settings indent + $settingsIndent = Get-ChildIndent $settings + $filterIndent = $settingsIndent + "`t" + } + + $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`"" + } + + "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 + } + + $queryEl.InnerText = Esc-Xml $Value + + Write-Host "[OK] Query replaced in dataset `"$dsName`"" + } +} + +# --- 8. 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" From 69213930b3c7f63ab253ee0885bf72af6cf173b2 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 17:38:13 +0300 Subject: [PATCH 27/35] Expand /skd-edit to 15 operations with batch mode and duplicate checks Add 9 new operations: add-dataParameter, add-order, add-selection, set-outputParameter, remove-field/total/calculated-field/parameter/filter. Add batch mode (;; separator), duplicate detection with skip, title support in fields and calculated fields, type support in calculated fields. Fix set-query double-escaping and Parse-CalcShorthand title extraction. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-edit/SKILL.md | 127 ++- .claude/skills/skd-edit/scripts/skd-edit.ps1 | 806 +++++++++++++++---- 2 files changed, 762 insertions(+), 171 deletions(-) diff --git a/.claude/skills/skd-edit/SKILL.md b/.claude/skills/skd-edit/SKILL.md index 8fd815e5..1a05278e 100644 --- a/.claude/skills/skd-edit/SKILL.md +++ b/.claude/skills/skd-edit/SKILL.md @@ -1,6 +1,6 @@ --- name: skd-edit -description: Точечное редактирование схемы компоновки данных 1С (СКД) — добавление полей, итогов, фильтров, параметров, вычисляемых полей +description: Точечное редактирование схемы компоновки данных 1С (СКД) — добавление/удаление полей, итогов, фильтров, параметров, вычисляемых полей argument-hint: -Operation -Value allowed-tools: - Bash @@ -11,14 +11,14 @@ allowed-tools: # /skd-edit — точечное редактирование СКД (Template.xml) -Атомарные операции модификации существующей схемы компоновки данных: добавление полей, итогов, фильтров, параметров, замена запроса. +Атомарные операции модификации существующей схемы компоновки данных: добавление и удаление полей, итогов, фильтров, параметров, настроек варианта, замена запроса. ## Параметры и команда | Параметр | Описание | |----------|----------| | `TemplatePath` | Путь к Template.xml (или к папке — автодополнение Ext/Template.xml) | -| `Operation` | Операция: `add-field`, `add-total`, `add-calculated-field`, `add-parameter`, `add-filter`, `set-query` | +| `Operation` | Операция (см. список ниже) | | `Value` | Значение операции (shorthand-строка или текст запроса) | | `DataSet` | (опц.) Имя набора данных (умолч. первый) | | `Variant` | (опц.) Имя варианта настроек (умолч. первый) | @@ -28,19 +28,31 @@ allowed-tools: 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`. Каждое значение обрабатывается последовательно. + ## Операции ### add-field — добавить поле в набор данных -Shorthand-формат из skd-compile: `"Цена: decimal(15,2)"`, `"Организация: CatalogRef.Организации @dimension"`. +Shorthand-формат: `"Имя [Заголовок]: тип @роль #ограничение"`. ```powershell -Operation add-field -Value "Цена: decimal(15,2)" --Operation add-field -Value "Организация: CatalogRef.Организации @dimension" +-Operation add-field -Value "Организация [Орг-ция]: CatalogRef.Организации @dimension" -Operation add-field -Value "Служебное: string #noFilter #noOrder" ``` -Поле добавляется перед `` в наборе, а также в `` первого варианта (если нет `-NoSelection`). +Поддержка заголовка (title) в квадратных скобках: `"Цена [Цена, руб.]: decimal(15,2)"`. + +Поле добавляется перед `` в наборе, а также в `` первого варианта (если нет `-NoSelection`). При дубликате dataPath — предупреждение, поле не добавляется. ### add-total — добавить итог @@ -49,13 +61,18 @@ Shorthand-формат из skd-compile: `"Цена: decimal(15,2)"`, `"Орга -Operation add-total -Value "Стоимость: Сумма(Кол * Цена)" ``` +При дубликате dataPath — предупреждение. + ### add-calculated-field — добавить вычисляемое поле +Формат: `"Имя [Заголовок]: тип = Выражение"` или `"Имя = Выражение"`. + ```powershell -Operation add-calculated-field -Value "Маржа = Продажа - Закупка" +-Operation add-calculated-field -Value "Наценка [Наценка, %]: decimal(10,2) = Маржа / Закупка * 100" ``` -Также добавляется в selection варианта (если нет `-NoSelection`). +Также добавляется в selection варианта (если нет `-NoSelection`). При дубликате dataPath — предупреждение. ### add-parameter — добавить параметр @@ -64,7 +81,7 @@ Shorthand-формат из skd-compile: `"Цена: decimal(15,2)"`, `"Орга -Operation add-parameter -Value "Организация: CatalogRef.Организации" ``` -`@autoDates` генерирует дополнительные параметры `ДатаНачала` и `ДатаОкончания`. +`@autoDates` генерирует дополнительные параметры `ДатаНачала` и `ДатаОкончания`. При дубликате name — предупреждение. ### add-filter — добавить фильтр в вариант настроек @@ -76,23 +93,113 @@ Shorthand-формат из skd-compile: `"Цена: decimal(15,2)"`, `"Орга Формат: `"Поле оператор значение @флаги"`. Флаги: `@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`. +### add-dataParameter — добавить параметр данных в вариант + +```powershell +-Operation add-dataParameter -Value "Период = LastMonth @user" +-Operation add-dataParameter -Value "Организация @off @user" +``` + +Формат: `"Имя [= значение] @флаги"`. Для StandardPeriod варианты (LastMonth, ThisYear и т.д.) распознаются автоматически. + +### add-order — добавить элемент сортировки в вариант + +```powershell +-Operation add-order -Value "Количество desc" +-Operation add-order -Value "Наименование" +-Operation add-order -Value "Auto" +``` + +Формат: `"Поле [desc]"`. По умолчанию — asc. `Auto` добавляет авто-элемент. + +### add-selection — добавить элемент выборки в вариант + +```powershell +-Operation add-selection -Value "Номенклатура" +-Operation add-selection -Value "Auto" +``` + ### set-query — заменить текст запроса ```powershell -Operation set-query -Value "ВЫБРАТЬ 1 КАК Тест" ``` +Не поддерживает пакетный режим. + +### set-outputParameter — установить параметр вывода + +```powershell +-Operation set-outputParameter -Value "Заголовок = Мой отчёт" +-Operation set-outputParameter -Value "ВыводитьЗаголовок = true" +``` + +Если параметр уже существует — заменяет значение. Поддерживаемые параметры: Заголовок/Title, ВыводитьЗаголовок/OutputTitle, ВертикальноеРасположениеОбщихИтогов/VerticalOverallPlacement, ГоризонтальноеРасположениеОбщихИтогов/HorizontalOverallPlacement, РасположениеРеквизитов/AttributePlacement, РасположениеГруппировки/GroupPlacement, РасположениеПолейГруппировки/GroupFieldsPlacement, РасположениеИтогов/OverallPlacement, РасположениеОтбора/FilterOutput, ВыводитьОтбор/OutputFilter. + +### remove-field — удалить поле из набора данных + +```powershell +-Operation remove-field -Value "Цена" +``` + +Удаляет поле с указанным dataPath. Также удаляет соответствующий элемент из selection варианта. + +### remove-total — удалить итог + +```powershell +-Operation remove-total -Value "Цена" +``` + +### remove-calculated-field — удалить вычисляемое поле + +```powershell +-Operation remove-calculated-field -Value "Маржа" +``` + +Также удаляет из selection варианта. + +### remove-parameter — удалить параметр + +```powershell +-Operation remove-parameter -Value "Организация" +``` + +### remove-filter — удалить фильтр из варианта + +```powershell +-Operation remove-filter -Value "Номенклатура" +``` + +Удаляет первый фильтр с указанным полем. + ## Примеры ```powershell -# Добавить числовое поле +# Добавить числовое поле с заголовком powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation add-field -Value "Цена: decimal(15,2)" + -TemplatePath test-tmp\edit-test.xml -Operation add-field -Value "Цена [Цена, руб.]: decimal(15,2)" + +# Пакетное добавление полей +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation add-field ` + -Value "Количество: decimal(15,3) ;; Сумма: decimal(15,2) ;; Валюта: string" # Добавить итог powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` -TemplatePath test-tmp\edit-test.xml -Operation add-total -Value "Цена: Среднее" +# Добавить параметр данных +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation add-dataParameter -Value "Период = LastMonth @user" + +# Установить заголовок +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation set-outputParameter -Value "Заголовок = Мой отчёт" + +# Удалить поле +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation remove-field -Value "Цена" + # Добавить фильтр powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` -TemplatePath test-tmp\edit-test.xml -Operation add-filter -Value "Организация = _ @off @user" diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1 index fcbc811c..091978e7 100644 --- a/.claude/skills/skd-edit/scripts/skd-edit.ps1 +++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1 @@ -3,8 +3,11 @@ [string]$TemplatePath, [Parameter(Mandatory)] - [ValidateSet("add-field","add-total","add-calculated-field", - "add-parameter","add-filter","set-query")] + [ValidateSet( + "add-field","add-total","add-calculated-field","add-parameter","add-filter", + "add-dataParameter","add-order","add-selection", + "set-query","set-outputParameter", + "remove-field","remove-total","remove-calculated-field","remove-parameter","remove-filter")] [string]$Operation, [Parameter(Mandatory)] @@ -60,6 +63,18 @@ $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 } @@ -86,7 +101,7 @@ function Resolve-TypeStr { return $typeStr } -# --- 3. Parsers (copied from skd-compile) --- +# --- 3. Parsers --- function Parse-FieldShorthand { param([string]$s) @@ -96,18 +111,27 @@ function Parse-FieldShorthand { 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 @@ -138,14 +162,28 @@ function Parse-TotalShorthand { function Parse-CalcShorthand { param([string]$s) - $idx = $s.IndexOf('=') - if ($idx -gt 0) { - return @{ - dataPath = $s.Substring(0, $idx).Trim() - expression = $s.Substring($idx + 1).Trim() - } + $title = "" + # Extract [Title] first + if ($s -match '\[([^\]]+)\]') { + $title = $Matches[1] + $s = $s -replace '\s*\[[^\]]+\]', '' } - return @{ dataPath = $s.Trim(); expression = "" } + + # 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 { @@ -243,6 +281,76 @@ function Parse-FilterShorthand { 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-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 { @@ -311,6 +419,18 @@ function Build-ValueTypeXml { 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" + return $lines -join "`r`n" +} + function Build-RoleXml { param([string[]]$roles, [string]$indent) @@ -361,6 +481,10 @@ function Build-FieldFragment { $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") } @@ -398,6 +522,17 @@ function Build-CalcFieldFragment { $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" } @@ -408,7 +543,6 @@ function Build-ParamFragment { $i = $indent $fragments = @() - # Main parameter $lines = @() $lines += "$i" $lines += "$i`t$(Esc-Xml $parsed.name)" @@ -439,7 +573,6 @@ function Build-ParamFragment { $lines += "$i" $fragments += ($lines -join "`r`n") - # @autoDates: generate ДатаНачала and ДатаОкончания if ($parsed.autoDates) { $paramName = $parsed.name @@ -506,9 +639,97 @@ function Build-SelectionItemFragment { $i = $indent $lines = @() - $lines += "$i" - $lines += "$i`t$(Esc-Xml $fieldName)" - $lines += "$i" + 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-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" } @@ -559,7 +780,6 @@ function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { $container.InsertBefore($ws, $refNode) | Out-Null $container.InsertBefore($newNode, $ws) | Out-Null } else { - # Append at end: insert before trailing whitespace $trailing = $container.LastChild if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { $container.InsertBefore($ws, $trailing) | Out-Null @@ -574,6 +794,19 @@ function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { } } +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') { @@ -601,6 +834,21 @@ function Find-LastElement($container, [string]$localName, [string]$nsUri) { 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 Resolve-DataSet { $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" $root = $xmlDoc.DocumentElement @@ -624,7 +872,6 @@ function Resolve-DataSet { exit 1 } - # First dataset foreach ($child in $root.ChildNodes) { if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) { return $child @@ -639,7 +886,6 @@ function Resolve-VariantSettings { $setNs = "http://v8.1c.ru/8.1/data-composition-system/settings" $root = $xmlDoc.DocumentElement - # Find settingsVariant $sv = $null if ($Variant) { foreach ($child in $root.ChildNodes) { @@ -674,7 +920,6 @@ function Resolve-VariantSettings { } } - # Find settings element inside variant foreach ($gc in $sv.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'settings' -and $gc.NamespaceURI -eq $setNs) { return $gc @@ -685,35 +930,45 @@ function Resolve-VariantSettings { 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 - $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) { - return $Variant - } - } - } - return $Variant - } else { - 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 - } + 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 } } } @@ -731,6 +986,15 @@ function Get-DataSetName($dsNode) { return "(unknown)" } +function Get-ContainerChildIndent($container) { + $ci = Get-ChildIndent $container + if (-not $container.HasChildNodes) { + $settingsIndent = Get-ChildIndent $container.ParentNode + $ci = $settingsIndent + "`t" + } + return $ci +} + # --- 6. Load XML --- $xmlDoc = New-Object System.Xml.XmlDocument @@ -741,34 +1005,47 @@ $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. Main logic --- +# --- 7. Batch value splitting --- + +if ($Operation -eq "set-query") { + $values = @($Value) +} else { + $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) +} + +# --- 8. Main logic --- switch ($Operation) { "add-field" { - $parsed = Parse-FieldShorthand $Value $dsNode = Resolve-DataSet $dsName = Get-DataSetName $dsNode - $childIndent = Get-ChildIndent $dsNode - $fragXml = Build-FieldFragment -parsed $parsed -indent $childIndent - $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($val in $values) { + $parsed = Parse-FieldShorthand $val + $childIndent = Get-ChildIndent $dsNode - # Insert before in dataset - $refNode = Find-FirstElement $dsNode @("dataSource") $schNs - foreach ($node in $nodes) { - Insert-BeforeElement $dsNode $node $refNode $childIndent - } + # 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 + } - Write-Host "[OK] Field `"$($parsed.dataPath)`" added to dataset `"$dsName`"" + $fragXml = Build-FieldFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml - # Add to selection of variant - if (-not $NoSelection) { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - $selNs = $setNs - $selection = Find-FirstElement $settings @("selection") $selNs - if ($selection) { - $selIndent = Get-ChildIndent $selection + $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" @() + $selIndent = Get-ContainerChildIndent $selection $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent $selNodes = Import-Fragment $xmlDoc $selXml foreach ($node in $selNodes) { @@ -780,64 +1057,76 @@ switch ($Operation) { } "add-total" { - $parsed = Parse-TotalShorthand $Value - $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + foreach ($val in $values) { + $parsed = Parse-TotalShorthand $val + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement - $fragXml = Build-TotalFragment -parsed $parsed -indent $childIndent - $nodes = Import-Fragment $xmlDoc $fragXml - - # Insert after last totalField, or before parameter/template/groupTemplate/settingsVariant - $root = $xmlDoc.DocumentElement - $lastTotal = Find-LastElement $root "totalField" $schNs - if ($lastTotal) { - # Insert after last totalField = before next sibling - $refNode = $lastTotal.NextSibling - # Skip whitespace nodes - while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { - $refNode = $refNode.NextSibling + # Duplicate check + $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "totalField" "dataPath" $parsed.dataPath $schNs + if ($existing) { + Write-Host "[WARN] TotalField `"$($parsed.dataPath)`" already exists — skipped" + continue } - } else { - $refNode = Find-FirstElement $root @("parameter","template","groupTemplate","settingsVariant") $schNs - } - foreach ($node in $nodes) { - Insert-BeforeElement $root $node $refNode $childIndent - } + $fragXml = Build-TotalFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml - Write-Host "[OK] TotalField `"$($parsed.dataPath)`" = $($parsed.expression) added" + $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" { - $parsed = Parse-CalcShorthand $Value - $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + foreach ($val in $values) { + $parsed = Parse-CalcShorthand $val + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement - $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 + # Duplicate check + $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "calculatedField" "dataPath" $parsed.dataPath $schNs + if ($existing) { + Write-Host "[WARN] CalculatedField `"$($parsed.dataPath)`" already exists — skipped" + continue } - } else { - $refNode = Find-FirstElement $root @("totalField","parameter","template","groupTemplate","settingsVariant") $schNs - } - foreach ($node in $nodes) { - Insert-BeforeElement $root $node $refNode $childIndent - } + $fragXml = Build-CalcFieldFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml - Write-Host "[OK] CalculatedField `"$($parsed.dataPath)`" = $($parsed.expression) added" + $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 + } - # Add to selection - if (-not $NoSelection) { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - $selection = Find-FirstElement $settings @("selection") $setNs - if ($selection) { - $selIndent = Get-ChildIndent $selection + 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" @() + $selIndent = Get-ContainerChildIndent $selection $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent $selNodes = Import-Fragment $xmlDoc $selXml foreach ($node in $selNodes) { @@ -849,76 +1138,123 @@ switch ($Operation) { } "add-parameter" { - $parsed = Parse-ParamShorthand $Value - $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + foreach ($val in $values) { + $parsed = Parse-ParamShorthand $val + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement - $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 + # Duplicate check + $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "parameter" "name" $parsed.name $schNs + if ($existing) { + Write-Host "[WARN] Parameter `"$($parsed.name)`" already exists — skipped" + continue } - } 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 + $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 } - } - Write-Host "[OK] Parameter `"$($parsed.name)`" added" - if ($parsed.autoDates) { - Write-Host "[OK] Auto-parameters `"ДатаНачала`", `"ДатаОкончания`" added" + 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" { - $parsed = Parse-FilterShorthand $Value $settings = Resolve-VariantSettings $varName = Get-VariantName - # Find or create - $filterEl = Find-FirstElement $settings @("filter") $setNs - if (-not $filterEl) { - # Create in settings - $settingsIndent = Get-ChildIndent $settings - $filterFragXml = "$settingsIndent" - $filterNodes = Import-Fragment $xmlDoc $filterFragXml - # Insert after selection or as first child - $refNode = Find-FirstElement $settings @("selection") $setNs - if ($refNode) { - $refNode = $refNode.NextSibling - while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { - $refNode = $refNode.NextSibling - } + 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 } - foreach ($node in $filterNodes) { - Insert-BeforeElement $settings $node $refNode $settingsIndent + + 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 } - $filterEl = Find-FirstElement $settings @("filter") $setNs - } - $filterIndent = Get-ChildIndent $filterEl - # If filter is self-closing (empty), we need a different indent - if (-not $filterEl.HasChildNodes) { - # Determine indent from settings indent - $settingsIndent = Get-ChildIndent $settings - $filterIndent = $settingsIndent + "`t" + Write-Host "[OK] DataParameter `"$($parsed.parameter)`" added to variant `"$varName`"" } + } - $fragXml = Build-FilterItemFragment -parsed $parsed -indent $filterIndent - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $filterEl $node $null $filterIndent + "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 + + $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`"" } + } - Write-Host "[OK] Filter `"$($parsed.field) $($parsed.op)`" 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" { @@ -931,13 +1267,161 @@ switch ($Operation) { exit 1 } - $queryEl.InnerText = Esc-Xml $Value + # 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 + } + } + } + + "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`"" + } + } } -# --- 8. Save --- +# --- 9. Save --- $content = $xmlDoc.OuterXml $content = $content -replace '(?<=<\?xml[^?]*encoding=")utf-8(?=")', 'UTF-8' From a9b7a90672fd6209f1e448642873206433b95b10 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 18:50:13 +0300 Subject: [PATCH 28/35] Expand /skd-edit to 22 operations with modify, clear, structure, and dataSetLink Add 7 new operations: modify-field, modify-filter, modify-dataParameter, clear-selection, clear-order, clear-filter, add-dataSetLink, set-structure. Add selection dedup for add-field/add-calculated-field and order dedup for add-order. Fix SetAttribute output leak. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-edit/SKILL.md | 95 ++- .claude/skills/skd-edit/scripts/skd-edit.ps1 | 593 ++++++++++++++++++- 2 files changed, 671 insertions(+), 17 deletions(-) diff --git a/.claude/skills/skd-edit/SKILL.md b/.claude/skills/skd-edit/SKILL.md index 1a05278e..4c57e648 100644 --- a/.claude/skills/skd-edit/SKILL.md +++ b/.claude/skills/skd-edit/SKILL.md @@ -11,7 +11,7 @@ allowed-tools: # /skd-edit — точечное редактирование СКД (Template.xml) -Атомарные операции модификации существующей схемы компоновки данных: добавление и удаление полей, итогов, фильтров, параметров, настроек варианта, замена запроса. +Атомарные операции модификации существующей схемы компоновки данных: добавление, удаление и модификация полей, итогов, фильтров, параметров, настроек варианта, управление структурой, замена запроса. ## Параметры и команда @@ -36,7 +36,7 @@ powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 -Te -Operation add-field -Value "Цена: decimal(15,2) ;; Количество: decimal(15,3) ;; Сумма: decimal(15,2)" ``` -Работает для всех операций кроме `set-query`. Каждое значение обрабатывается последовательно. +Работает для всех операций кроме `set-query` и `set-structure`. Каждое значение обрабатывается последовательно. ## Операции @@ -119,6 +119,15 @@ Shorthand-формат: `"Имя [Заголовок]: тип @роль #огр -Operation add-selection -Value "Auto" ``` +### add-dataSetLink — добавить связь наборов данных + +```powershell +-Operation add-dataSetLink -Value "Набор1 > Набор2 on Поле1 = Поле2" +-Operation add-dataSetLink -Value "Набор1 > Набор2 on Поле1 = Поле2 [param Связь]" +``` + +Формат: `"Источник > Приёмник on ВыражениеИсточника = ВыражениеПриёмника [param ИмяПараметра]"`. Параметр связи опционален. + ### set-query — заменить текст запроса ```powershell @@ -136,6 +145,66 @@ Shorthand-формат: `"Имя [Заголовок]: тип @роль #огр Если параметр уже существует — заменяет значение. Поддерживаемые параметры: Заголовок/Title, ВыводитьЗаголовок/OutputTitle, ВертикальноеРасположениеОбщихИтогов/VerticalOverallPlacement, ГоризонтальноеРасположениеОбщихИтогов/HorizontalOverallPlacement, РасположениеРеквизитов/AttributePlacement, РасположениеГруппировки/GroupPlacement, РасположениеПолейГруппировки/GroupFieldsPlacement, РасположениеИтогов/OverallPlacement, РасположениеОтбора/FilterOutput, ВыводитьОтбор/OutputFilter. +### set-structure — установить структуру варианта + +```powershell +-Operation set-structure -Value "Организация > Номенклатура > details" +-Operation set-structure -Value "details" +``` + +Формат: `"ПолеГруппировки1 > ПолеГруппировки2 > details"`. `details`/`детали` — группа детальных записей (пустой groupBy). Заменяет всю существующую структуру варианта. Не поддерживает пакетный режим. + +### modify-field — изменить существующее поле в наборе данных + +```powershell +-Operation modify-field -Value "Цена [Цена USD]: decimal(10,4) @dimension" +-Operation modify-field -Value "Цена: decimal(10,2)" +``` + +Использует тот же shorthand что и `add-field`. Находит поле по dataPath, объединяет с переданными свойствами (непустые переопределяют), сохраняет позицию. Если тип/заголовок/роли/ограничения не указаны в shorthand — берутся из существующего поля. + +### modify-filter — изменить существующий фильтр в варианте + +```powershell +-Operation modify-filter -Value "Организация >= bar @off" +-Operation modify-filter -Value "Дата = 2025-01-01T00:00:00 @user @quickAccess" +``` + +Находит фильтр по полю (left), обновляет оператор, значение и флаги. Используется тот же shorthand что и `add-filter`. + +### modify-dataParameter — изменить существующий параметр данных + +```powershell +-Operation modify-dataParameter -Value "Период = ThisYear" +-Operation modify-dataParameter -Value "Организация @off @user" +``` + +Находит dataParameter по имени, обновляет значение и флаги. Используется тот же shorthand что и `add-dataParameter`. + +### clear-selection — очистить выборку варианта + +```powershell +-Operation clear-selection -Value "*" +``` + +Удаляет все элементы из ``. Value игнорируется. + +### clear-order — очистить сортировку варианта + +```powershell +-Operation clear-order -Value "*" +``` + +Удаляет все элементы из ``. Value игнорируется. + +### clear-filter — очистить фильтры варианта + +```powershell +-Operation clear-filter -Value "*" +``` + +Удаляет все элементы из ``. Value игнорируется. + ### remove-field — удалить поле из набора данных ```powershell @@ -207,6 +276,28 @@ powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` # Заменить запрос powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` -TemplatePath test-tmp\edit-test.xml -Operation set-query -Value "ВЫБРАТЬ 1 КАК Тест" + +# Установить структуру группировок +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation set-structure -Value "Организация > Номенклатура > details" + +# Модифицировать поле +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation modify-field -Value "Цена [Цена USD]: decimal(10,4) @dimension" + +# Модифицировать фильтр +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation modify-filter -Value "Организация >= bar @off" + +# Очистить выборку и установить заново +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation clear-selection -Value "*" +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation add-selection -Value "Auto" + +# Добавить связь наборов данных +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation add-dataSetLink -Value "Набор1 > Набор2 on Поле1 = Поле2" ``` ## Верификация diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1 index 091978e7..1229e9b5 100644 --- a/.claude/skills/skd-edit/scripts/skd-edit.ps1 +++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1 @@ -5,8 +5,10 @@ [Parameter(Mandatory)] [ValidateSet( "add-field","add-total","add-calculated-field","add-parameter","add-filter", - "add-dataParameter","add-order","add-selection", - "set-query","set-outputParameter", + "add-dataParameter","add-order","add-selection","add-dataSetLink", + "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, @@ -145,6 +147,66 @@ function Parse-FieldShorthand { 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) @@ -339,6 +401,58 @@ function Parse-OrderShorthand { 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-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('=') @@ -705,6 +819,70 @@ function Build-OrderItemFragment { 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-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) @@ -794,6 +972,18 @@ function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { } } +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 @@ -849,6 +1039,52 @@ function Find-ElementByChildValue($container, [string]$elemName, [string]$childN 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)" + $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)" + $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 @@ -1007,7 +1243,7 @@ $corNs = "http://v8.1c.ru/8.1/data-composition-system/core" # --- 7. Batch value splitting --- -if ($Operation -eq "set-query") { +if ($Operation -eq "set-query" -or $Operation -eq "set-structure") { $values = @($Value) } else { $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) @@ -1045,13 +1281,18 @@ switch ($Operation) { $settings = Resolve-VariantSettings $varName = Get-VariantName $selection = Ensure-SettingsChild $settings "selection" @() - $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 + $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`"" } - Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`"" } } } @@ -1126,13 +1367,18 @@ switch ($Operation) { $settings = Resolve-VariantSettings $varName = Get-VariantName $selection = Ensure-SettingsChild $settings "selection" @() - $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 + $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`"" } - Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`"" } } } @@ -1226,6 +1472,27 @@ switch ($Operation) { $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) { @@ -1300,6 +1567,302 @@ switch ($Operation) { } } + "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" + } + } + + "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 From 2d0835ba17001d37ed999c46ee3d3e07411082f4 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 19:39:07 +0300 Subject: [PATCH 29/35] Fix XML formatting in batch insert and after clear operations Two bugs caused tag concatenation (e.g. ` --- .claude/skills/skd-edit/scripts/skd-edit.ps1 | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1 index 1229e9b5..33628046 100644 --- a/.claude/skills/skd-edit/scripts/skd-edit.ps1 +++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1 @@ -961,7 +961,7 @@ function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { $trailing = $container.LastChild if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { $container.InsertBefore($ws, $trailing) | Out-Null - $container.InsertBefore($newNode, $ws) | Out-Null + $container.InsertBefore($newNode, $trailing) | Out-Null } else { $container.AppendChild($ws) | Out-Null $container.AppendChild($newNode) | Out-Null @@ -1223,12 +1223,16 @@ function Get-DataSetName($dsNode) { } function Get-ContainerChildIndent($container) { - $ci = Get-ChildIndent $container - if (-not $container.HasChildNodes) { - $settingsIndent = Get-ChildIndent $container.ParentNode - $ci = $settingsIndent + "`t" + $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" } - return $ci } # --- 6. Load XML --- From 703c3e6f8019060569d66c2b49998c405d03df34 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 20:01:10 +0300 Subject: [PATCH 30/35] Add version headers with source URL to all 22 skill scripts Each .ps1 now starts with skill name, version (v1.0), and repo link. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/epf-add-form/scripts/add-form.ps1 | 4 +++- .claude/skills/epf-add-help/scripts/add-help.ps1 | 4 +++- .claude/skills/epf-add-template/scripts/add-template.ps1 | 4 +++- .claude/skills/epf-init/scripts/init.ps1 | 4 +++- .claude/skills/epf-remove-form/scripts/remove-form.ps1 | 4 +++- .../skills/epf-remove-template/scripts/remove-template.ps1 | 4 +++- .claude/skills/form-add/scripts/form-add.ps1 | 4 +++- .claude/skills/form-compile/scripts/form-compile.ps1 | 4 +++- .claude/skills/form-edit/scripts/form-edit.ps1 | 4 +++- .claude/skills/form-info/scripts/form-info.ps1 | 4 +++- .claude/skills/form-validate/scripts/form-validate.ps1 | 4 +++- .claude/skills/mxl-compile/scripts/mxl-compile.ps1 | 4 +++- .claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 | 4 +++- .claude/skills/mxl-info/scripts/mxl-info.ps1 | 4 +++- .claude/skills/mxl-validate/scripts/mxl-validate.ps1 | 4 +++- .claude/skills/role-compile/scripts/role-compile.ps1 | 4 +++- .claude/skills/role-info/scripts/role-info.ps1 | 4 +++- .claude/skills/role-validate/scripts/role-validate.ps1 | 4 +++- .claude/skills/skd-compile/scripts/skd-compile.ps1 | 4 +++- .claude/skills/skd-edit/scripts/skd-edit.ps1 | 4 +++- .claude/skills/skd-info/scripts/skd-info.ps1 | 4 +++- .claude/skills/skd-validate/scripts/skd-validate.ps1 | 4 +++- 22 files changed, 66 insertions(+), 22 deletions(-) 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/scripts/add-template.ps1 b/.claude/skills/epf-add-template/scripts/add-template.ps1 index 456d3444..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, 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/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 934986d2..6df434ed 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -1,4 +1,6 @@ -param( +# skd-compile v1.0 — Compile 1C DCS from JSON +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$JsonPath, diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1 index 33628046..8204d70a 100644 --- a/.claude/skills/skd-edit/scripts/skd-edit.ps1 +++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1 @@ -1,4 +1,6 @@ -param( +# skd-edit v1.0 — Atomic 1C DCS editor +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$TemplatePath, diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index 636903b8..6dfc8a60 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -1,4 +1,6 @@ -param( +# 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")] diff --git a/.claude/skills/skd-validate/scripts/skd-validate.ps1 b/.claude/skills/skd-validate/scripts/skd-validate.ps1 index 1a886ca6..2baa86c0 100644 --- a/.claude/skills/skd-validate/scripts/skd-validate.ps1 +++ b/.claude/skills/skd-validate/scripts/skd-validate.ps1 @@ -1,4 +1,6 @@ -param( +# skd-validate v1.0 — Validate 1C DCS structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$TemplatePath, From 28b8061d64e4f83fe92c0cd004bbccea18f8d7c6 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 20:10:49 +0300 Subject: [PATCH 31/35] Compact skd-edit SKILL.md: remove redundant examples section Reduce from 309 to 183 lines by removing duplicate full-command examples (already shown inline per operation) and consolidating remove-*/clear-* into a single table. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-edit/SKILL.md | 274 +++++++++---------------------- 1 file changed, 74 insertions(+), 200 deletions(-) diff --git a/.claude/skills/skd-edit/SKILL.md b/.claude/skills/skd-edit/SKILL.md index 4c57e648..987f2de7 100644 --- a/.claude/skills/skd-edit/SKILL.md +++ b/.claude/skills/skd-edit/SKILL.md @@ -36,269 +36,143 @@ powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 -Te -Operation add-field -Value "Цена: decimal(15,2) ;; Количество: decimal(15,3) ;; Сумма: decimal(15,2)" ``` -Работает для всех операций кроме `set-query` и `set-structure`. Каждое значение обрабатывается последовательно. +Работает для всех операций кроме `set-query` и `set-structure`. ## Операции ### add-field — добавить поле в набор данных -Shorthand-формат: `"Имя [Заголовок]: тип @роль #ограничение"`. +Shorthand: `"Имя [Заголовок]: тип @роль #ограничение"`. -```powershell --Operation add-field -Value "Цена: decimal(15,2)" --Operation add-field -Value "Организация [Орг-ция]: CatalogRef.Организации @dimension" --Operation add-field -Value "Служебное: string #noFilter #noOrder" +``` +"Цена: decimal(15,2)" +"Организация [Орг-ция]: CatalogRef.Организации @dimension" +"Служебное: string #noFilter #noOrder" ``` -Поддержка заголовка (title) в квадратных скобках: `"Цена [Цена, руб.]: decimal(15,2)"`. - -Поле добавляется перед `` в наборе, а также в `` первого варианта (если нет `-NoSelection`). При дубликате dataPath — предупреждение, поле не добавляется. +Поле добавляется в набор и в selection варианта (если нет `-NoSelection`). Дубликат dataPath — предупреждение, пропуск. ### add-total — добавить итог -```powershell --Operation add-total -Value "Цена: Среднее" --Operation add-total -Value "Стоимость: Сумма(Кол * Цена)" ``` - -При дубликате dataPath — предупреждение. +"Цена: Среднее" +"Стоимость: Сумма(Кол * Цена)" +``` ### add-calculated-field — добавить вычисляемое поле -Формат: `"Имя [Заголовок]: тип = Выражение"` или `"Имя = Выражение"`. +Shorthand: `"Имя [Заголовок]: тип = Выражение"`. -```powershell --Operation add-calculated-field -Value "Маржа = Продажа - Закупка" --Operation add-calculated-field -Value "Наценка [Наценка, %]: decimal(10,2) = Маржа / Закупка * 100" +``` +"Маржа = Продажа - Закупка" +"Наценка [Наценка, %]: decimal(10,2) = Маржа / Закупка * 100" ``` -Также добавляется в selection варианта (если нет `-NoSelection`). При дубликате dataPath — предупреждение. +Также добавляется в selection варианта. ### add-parameter — добавить параметр -```powershell --Operation add-parameter -Value "Период: StandardPeriod = LastMonth @autoDates" --Operation add-parameter -Value "Организация: CatalogRef.Организации" +``` +"Период: StandardPeriod = LastMonth @autoDates" +"Организация: CatalogRef.Организации" ``` -`@autoDates` генерирует дополнительные параметры `ДатаНачала` и `ДатаОкончания`. При дубликате name — предупреждение. +`@autoDates` генерирует `ДатаНачала` и `ДатаОкончания` автоматически. -### add-filter — добавить фильтр в вариант настроек +### add-filter — добавить фильтр в вариант + +Shorthand: `"Поле оператор значение @флаги"`. Флаги: `@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`. -```powershell --Operation add-filter -Value "Номенклатура = _ @off @user" --Operation add-filter -Value "Дата >= 2024-01-01T00:00:00" --Operation add-filter -Value "Статус filled" ``` - -Формат: `"Поле оператор значение @флаги"`. Флаги: `@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`. +"Номенклатура = _ @off @user" +"Дата >= 2024-01-01T00:00:00" +"Статус filled" +``` ### add-dataParameter — добавить параметр данных в вариант -```powershell --Operation add-dataParameter -Value "Период = LastMonth @user" --Operation add-dataParameter -Value "Организация @off @user" +Shorthand: `"Имя [= значение] @флаги"`. + +``` +"Период = LastMonth @user" +"Организация @off @user" ``` -Формат: `"Имя [= значение] @флаги"`. Для StandardPeriod варианты (LastMonth, ThisYear и т.д.) распознаются автоматически. +### add-order — добавить сортировку -### add-order — добавить элемент сортировки в вариант +Shorthand: `"Поле [desc]"`. По умолчанию asc. `Auto` — авто-элемент. -```powershell --Operation add-order -Value "Количество desc" --Operation add-order -Value "Наименование" --Operation add-order -Value "Auto" +``` +"Количество desc" +"Auto" ``` -Формат: `"Поле [desc]"`. По умолчанию — asc. `Auto` добавляет авто-элемент. +### add-selection — добавить элемент выборки -### add-selection — добавить элемент выборки в вариант - -```powershell --Operation add-selection -Value "Номенклатура" --Operation add-selection -Value "Auto" +``` +"Номенклатура" +"Auto" ``` ### add-dataSetLink — добавить связь наборов данных -```powershell --Operation add-dataSetLink -Value "Набор1 > Набор2 on Поле1 = Поле2" --Operation add-dataSetLink -Value "Набор1 > Набор2 on Поле1 = Поле2 [param Связь]" -``` +Shorthand: `"Источник > Приёмник on ВырИсточника = ВырПриёмника [param Имя]"`. -Формат: `"Источник > Приёмник on ВыражениеИсточника = ВыражениеПриёмника [param ИмяПараметра]"`. Параметр связи опционален. +``` +"Набор1 > Набор2 on Поле1 = Поле2" +"Набор1 > Набор2 on Поле1 = Поле2 [param Связь]" +``` ### set-query — заменить текст запроса -```powershell --Operation set-query -Value "ВЫБРАТЬ 1 КАК Тест" -``` - -Не поддерживает пакетный режим. +Не поддерживает пакетный режим. Value — полный текст запроса. ### set-outputParameter — установить параметр вывода -```powershell --Operation set-outputParameter -Value "Заголовок = Мой отчёт" --Operation set-outputParameter -Value "ВыводитьЗаголовок = true" +``` +"Заголовок = Мой отчёт" +"ВыводитьЗаголовок = true" ``` -Если параметр уже существует — заменяет значение. Поддерживаемые параметры: Заголовок/Title, ВыводитьЗаголовок/OutputTitle, ВертикальноеРасположениеОбщихИтогов/VerticalOverallPlacement, ГоризонтальноеРасположениеОбщихИтогов/HorizontalOverallPlacement, РасположениеРеквизитов/AttributePlacement, РасположениеГруппировки/GroupPlacement, РасположениеПолейГруппировки/GroupFieldsPlacement, РасположениеИтогов/OverallPlacement, РасположениеОтбора/FilterOutput, ВыводитьОтбор/OutputFilter. +Если параметр уже существует — заменяет значение. ### set-structure — установить структуру варианта -```powershell --Operation set-structure -Value "Организация > Номенклатура > details" --Operation set-structure -Value "details" +Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — детальные записи. Заменяет всю структуру. Не поддерживает пакетный режим. + +``` +"Организация > Номенклатура > details" +"details" ``` -Формат: `"ПолеГруппировки1 > ПолеГруппировки2 > details"`. `details`/`детали` — группа детальных записей (пустой groupBy). Заменяет всю существующую структуру варианта. Не поддерживает пакетный режим. +### modify-field — изменить существующее поле -### modify-field — изменить существующее поле в наборе данных +Тот же shorthand что и `add-field`. Находит по dataPath, объединяет свойства (непустые переопределяют), сохраняет позицию. -```powershell --Operation modify-field -Value "Цена [Цена USD]: decimal(10,4) @dimension" --Operation modify-field -Value "Цена: decimal(10,2)" +``` +"Цена [Цена USD]: decimal(10,4) @dimension" ``` -Использует тот же shorthand что и `add-field`. Находит поле по dataPath, объединяет с переданными свойствами (непустые переопределяют), сохраняет позицию. Если тип/заголовок/роли/ограничения не указаны в shorthand — берутся из существующего поля. +### modify-filter — изменить существующий фильтр -### modify-filter — изменить существующий фильтр в варианте +Тот же shorthand что и `add-filter`. Находит по полю, обновляет оператор/значение/флаги. -```powershell --Operation modify-filter -Value "Организация >= bar @off" --Operation modify-filter -Value "Дата = 2025-01-01T00:00:00 @user @quickAccess" -``` +### modify-dataParameter — изменить параметр данных -Находит фильтр по полю (left), обновляет оператор, значение и флаги. Используется тот же shorthand что и `add-filter`. +Тот же shorthand что и `add-dataParameter`. Находит по имени, обновляет значение/флаги. -### modify-dataParameter — изменить существующий параметр данных +### remove-* и clear-* -```powershell --Operation modify-dataParameter -Value "Период = ThisYear" --Operation modify-dataParameter -Value "Организация @off @user" -``` - -Находит dataParameter по имени, обновляет значение и флаги. Используется тот же shorthand что и `add-dataParameter`. - -### clear-selection — очистить выборку варианта - -```powershell --Operation clear-selection -Value "*" -``` - -Удаляет все элементы из ``. Value игнорируется. - -### clear-order — очистить сортировку варианта - -```powershell --Operation clear-order -Value "*" -``` - -Удаляет все элементы из ``. Value игнорируется. - -### clear-filter — очистить фильтры варианта - -```powershell --Operation clear-filter -Value "*" -``` - -Удаляет все элементы из ``. Value игнорируется. - -### remove-field — удалить поле из набора данных - -```powershell --Operation remove-field -Value "Цена" -``` - -Удаляет поле с указанным dataPath. Также удаляет соответствующий элемент из selection варианта. - -### remove-total — удалить итог - -```powershell --Operation remove-total -Value "Цена" -``` - -### remove-calculated-field — удалить вычисляемое поле - -```powershell --Operation remove-calculated-field -Value "Маржа" -``` - -Также удаляет из selection варианта. - -### remove-parameter — удалить параметр - -```powershell --Operation remove-parameter -Value "Организация" -``` - -### remove-filter — удалить фильтр из варианта - -```powershell --Operation remove-filter -Value "Номенклатура" -``` - -Удаляет первый фильтр с указанным полем. - -## Примеры - -```powershell -# Добавить числовое поле с заголовком -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation add-field -Value "Цена [Цена, руб.]: decimal(15,2)" - -# Пакетное добавление полей -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation add-field ` - -Value "Количество: decimal(15,3) ;; Сумма: decimal(15,2) ;; Валюта: string" - -# Добавить итог -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation add-total -Value "Цена: Среднее" - -# Добавить параметр данных -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation add-dataParameter -Value "Период = LastMonth @user" - -# Установить заголовок -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation set-outputParameter -Value "Заголовок = Мой отчёт" - -# Удалить поле -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation remove-field -Value "Цена" - -# Добавить фильтр -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation add-filter -Value "Организация = _ @off @user" - -# Заменить запрос -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation set-query -Value "ВЫБРАТЬ 1 КАК Тест" - -# Установить структуру группировок -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation set-structure -Value "Организация > Номенклатура > details" - -# Модифицировать поле -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation modify-field -Value "Цена [Цена USD]: decimal(10,4) @dimension" - -# Модифицировать фильтр -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation modify-filter -Value "Организация >= bar @off" - -# Очистить выборку и установить заново -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation clear-selection -Value "*" -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation add-selection -Value "Auto" - -# Добавить связь наборов данных -powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` - -TemplatePath test-tmp\edit-test.xml -Operation add-dataSetLink -Value "Набор1 > Набор2 on Поле1 = Поле2" -``` +| Операция | 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 | ## Верификация From acb7a4eadcb633af8c62b3ed36792c8a0a465b0a Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 21:10:53 +0300 Subject: [PATCH 32/35] Expand /skd-edit to 25 operations with add-dataSet, add-variant, add-conditionalAppearance Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-edit/SKILL.md | 37 ++- .claude/skills/skd-edit/scripts/skd-edit.ps1 | 295 ++++++++++++++++++- 2 files changed, 329 insertions(+), 3 deletions(-) diff --git a/.claude/skills/skd-edit/SKILL.md b/.claude/skills/skd-edit/SKILL.md index 987f2de7..7d42864b 100644 --- a/.claude/skills/skd-edit/SKILL.md +++ b/.claude/skills/skd-edit/SKILL.md @@ -36,7 +36,7 @@ powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 -Te -Operation add-field -Value "Цена: decimal(15,2) ;; Количество: decimal(15,3) ;; Сумма: decimal(15,2)" ``` -Работает для всех операций кроме `set-query` и `set-structure`. +Работает для всех операций кроме `set-query`, `set-structure` и `add-dataSet`. ## Операции @@ -123,6 +123,41 @@ Shorthand: `"Источник > Приёмник on ВырИсточника = "Набор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]"`. + +``` +"ЦветТекста = web:Red when Сумма < 0" +"ЦветФона = web:LightGreen when Статус = Одобрен for Статус" +"МинимальнаяШирина = 50 for Организация" +"Формат = ЧДЦ=2 for Цена, Сумма" +``` + +Типы значений (автодетект): `web:*`/`style:*`/`win:*` → цвет, `true`/`false` → boolean, иначе строка. + ### set-query — заменить текст запроса Не поддерживает пакетный режим. Value — полный текст запроса. diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1 index 8204d70a..6d92c3e3 100644 --- a/.claude/skills/skd-edit/scripts/skd-edit.ps1 +++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1 @@ -1,4 +1,4 @@ -# skd-edit v1.0 — Atomic 1C DCS editor +# skd-edit v1.1 — Atomic 1C DCS editor # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -8,6 +8,7 @@ param( [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", @@ -428,6 +429,78 @@ function Parse-DataSetLinkShorthand { 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) @@ -838,6 +911,104 @@ function Build-DataSetLinkFragment { 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) @@ -1249,7 +1420,7 @@ $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") { +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 { $_ }) @@ -1637,6 +1808,126 @@ switch ($Operation) { } } + "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 From 2f2ecca88bdf43474acde5c1f4e166f5e166ec04 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 21:13:27 +0300 Subject: [PATCH 33/35] Clarify add-conditionalAppearance shorthand syntax in SKILL.md Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-edit/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/skd-edit/SKILL.md b/.claude/skills/skd-edit/SKILL.md index 7d42864b..26eb1d2a 100644 --- a/.claude/skills/skd-edit/SKILL.md +++ b/.claude/skills/skd-edit/SKILL.md @@ -147,7 +147,7 @@ Shorthand: `"Имя [Представление]"`. Представление ### add-conditionalAppearance — добавить условное оформление -Shorthand: `"Параметр = значение [when Поле оп правое] [for Поле1, Поле2]"`. +Shorthand: `"Параметр = значение [when условие] [for Поле1, Поле2]"`. Блок `when` — синтаксис `add-filter` (Поле оператор значение). ``` "ЦветТекста = web:Red when Сумма < 0" From 5c0a55e090d471430f1db8d2039b6bac001f8a53 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 21:56:17 +0300 Subject: [PATCH 34/35] Add /skd-edit to README and skd-guide documentation Co-Authored-By: Claude Opus 4.6 --- README.md | 3 ++- docs/skd-guide.md | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9a63716d..40a9ee73 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ | Табличный документ (MXL) | 4 навыка `/mxl-*` | Анализ, создание, компиляция макетов печатных форм | [Подробнее](docs/mxl-guide.md) | | Управляемые формы (Form) | 6 навыков `/form-*` | Создание, анализ, генерация, модификация, валидация управляемых форм | [Подробнее](docs/form-guide.md) | | Роли (Role) | 3 навыка `/role-*` | Анализ прав роли, создание из JSON DSL, валидация | [Подробнее](docs/role-guide.md) | -| Схема компоновки (СКД) | 3 навыка `/skd-*` | Анализ, генерация из JSON DSL, валидация схем компоновки данных | [Подробнее](docs/skd-guide.md) | +| Схема компоновки (СКД) | 4 навыка `/skd-*` | Анализ, генерация из JSON DSL, точечное редактирование, валидация схем компоновки данных | [Подробнее](docs/skd-guide.md) | | Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — | ## Требования @@ -74,6 +74,7 @@ ├── role-validate/ # Валидация роли ├── skd-info/ # Анализ схемы компоновки данных ├── skd-compile/ # Компиляция СКД из JSON DSL +├── skd-edit/ # Точечное редактирование СКД (25 операций) ├── skd-validate/ # Валидация СКД └── img-grid/ # Сетка для анализа изображений docs/ diff --git a/docs/skd-guide.md b/docs/skd-guide.md index 98eaeb67..7da5f855 100644 --- a/docs/skd-guide.md +++ b/docs/skd-guide.md @@ -1,6 +1,6 @@ # Схема компоновки данных (СКД) -Навыки группы `/skd-*` позволяют анализировать, создавать и проверять схемы компоновки данных 1С — XML-файлы DataCompositionSchema (Template.xml). +Навыки группы `/skd-*` позволяют анализировать, создавать, редактировать и проверять схемы компоновки данных 1С — XML-файлы DataCompositionSchema (Template.xml). ## Навыки @@ -8,19 +8,21 @@ |-------|-----------|----------| | `/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-info + ↕ /skd-edit → /skd-info ``` 1. Claude формирует JSON-определение СКД (shorthand-поля, параметры, итоги, варианты) 2. `/skd-compile` генерирует Template.xml с корректными namespace, типами, группировками -3. `/skd-validate` проверяет корректность сгенерированного XML -4. `/skd-info` выводит компактную сводку для визуальной проверки +3. `/skd-edit` вносит точечные изменения: добавление полей, фильтров, наборов данных, вариантов, условного оформления и т.д. +4. `/skd-validate` проверяет корректность XML +5. `/skd-info` выводит компактную сводку для визуальной проверки ## JSON DSL — компактный формат From 89c0775c65364e70e65c96c55bf3d1bb1bf11edb Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 22:00:12 +0300 Subject: [PATCH 35/35] Add work-in-progress notice to README Co-Authored-By: Claude Opus 4.6 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 40a9ee73..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-исходников, не запоминая детали формата. ## Быстрый старт