diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 62754ccd..ff745829 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -1,4 +1,4 @@ -# skd-compile v1.30 — Compile 1C DCS from JSON +# skd-compile v1.31 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, @@ -1787,6 +1787,51 @@ function Emit-GroupTemplates { # === Settings Variants === +function Emit-SelectionItem { + param($item, [string]$indent) + if ($item -is [string]) { + if ($item -eq "Auto") { + X "$indent" + } else { + X "$indent" + X "$indent`t$(Esc-Xml $item)" + X "$indent" + } + return + } + if ($item.folder -or (Has-JsonProp $item 'folder')) { + X "$indent" + # Optional на folder (редкий случай, для round-trip-целостности) + if ($item.field) { + X "$indent`t$(Esc-Xml "$($item.field)")" + } + X "$indent`t" + X "$indent`t`t" + X "$indent`t`t`tru" + X "$indent`t`t`t$(Esc-Xml "$($item.folder)")" + X "$indent`t`t" + X "$indent`t" + foreach ($sub in $item.items) { + Emit-SelectionItem -item $sub -indent "$indent`t" + } + X "$indent`tAuto" + X "$indent" + return + } + # field with optional title + X "$indent" + X "$indent`t$(Esc-Xml "$($item.field)")" + if ($item.title) { + X "$indent`t" + X "$indent`t`t" + X "$indent`t`t`tru" + X "$indent`t`t`t$(Esc-Xml "$($item.title)")" + X "$indent`t`t" + X "$indent`t" + } + X "$indent" +} + function Emit-Selection { param($items, [string]$indent, [switch]$skipAuto) @@ -1794,45 +1839,8 @@ function Emit-Selection { X "$indent" foreach ($item in $items) { - if ($item -is [string]) { - if ($item -eq "Auto") { - if (-not $skipAuto) { - X "$indent`t" - } - } else { - X "$indent`t" - X "$indent`t`t$(Esc-Xml $item)" - X "$indent`t" - } - } elseif ($item.folder) { - X "$indent`t" - X "$indent`t`t" - X "$indent`t`t`t" - X "$indent`t`t`t`tru" - X "$indent`t`t`t`t$(Esc-Xml "$($item.folder)")" - X "$indent`t`t`t" - X "$indent`t`t" - foreach ($sub in $item.items) { - $subName = if ($sub -is [string]) { $sub } else { "$($sub.field)" } - X "$indent`t`t" - X "$indent`t`t`t$(Esc-Xml $subName)" - X "$indent`t`t" - } - X "$indent`t`tAuto" - X "$indent`t" - } else { - X "$indent`t" - X "$indent`t`t$(Esc-Xml "$($item.field)")" - if ($item.title) { - X "$indent`t`t" - X "$indent`t`t`t" - X "$indent`t`t`t`tru" - X "$indent`t`t`t`t$(Esc-Xml "$($item.title)")" - X "$indent`t`t`t" - X "$indent`t`t" - } - X "$indent`t" - } + if ($skipAuto -and ($item -is [string]) -and $item -eq 'Auto') { continue } + Emit-SelectionItem -item $item -indent "$indent`t" } X "$indent" } @@ -2370,6 +2378,21 @@ function Emit-StructureItem { X "$indent" } + elseif ($type -eq "nestedObject") { + X "$indent" + if ($item.objectID) { X "$indent`t$(Esc-Xml "$($item.objectID)")" } + X "$indent`t" + $s = $item.settings + if ($s) { + if ($s.selection) { Emit-Selection -items $s.selection -indent "$indent`t`t" } + if ($s.filter) { Emit-Filter -items $s.filter -indent "$indent`t`t" } + if ($s.order) { Emit-Order -items $s.order -indent "$indent`t`t" } + if ($s.conditionalAppearance) { Emit-ConditionalAppearance -items $s.conditionalAppearance -indent "$indent`t`t" } + if ($s.outputParameters) { Emit-OutputParameters -params $s.outputParameters -indent "$indent`t`t" } + } + X "$indent`t" + X "$indent" + } } function Emit-SettingsVariants { diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py index 6bc8d86e..667600be 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.py +++ b/.claude/skills/skd-compile/scripts/skd-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# skd-compile v1.30 — Compile 1C DCS from JSON +# skd-compile v1.31 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -1481,46 +1481,51 @@ def emit_group_templates(lines, defn): # === Settings Variants === +def emit_selection_item(lines, item, indent): + if isinstance(item, str): + if item == 'Auto': + lines.append(f'{indent}') + else: + lines.append(f'{indent}') + lines.append(f'{indent}\t{esc_xml(item)}') + lines.append(f'{indent}') + return + if 'folder' in item: + lines.append(f'{indent}') + if item.get('field'): + lines.append(f'{indent}\t{esc_xml(str(item["field"]))}') + lines.append(f'{indent}\t') + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t\tru') + lines.append(f'{indent}\t\t\t{esc_xml(str(item["folder"]))}') + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t') + for sub in (item.get('items') or []): + emit_selection_item(lines, sub, f'{indent}\t') + lines.append(f'{indent}\tAuto') + lines.append(f'{indent}') + return + # field with optional title + lines.append(f'{indent}') + lines.append(f'{indent}\t{esc_xml(str(item["field"]))}') + if item.get('title'): + lines.append(f'{indent}\t') + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t\tru') + lines.append(f'{indent}\t\t\t{esc_xml(str(item["title"]))}') + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t') + lines.append(f'{indent}') + + def emit_selection(lines, items, indent, skip_auto=False): if not items or len(items) == 0: return - lines.append(f'{indent}') for item in items: - if isinstance(item, str): - if item == 'Auto': - if not skip_auto: - lines.append(f'{indent}\t') - else: - lines.append(f'{indent}\t') - lines.append(f'{indent}\t\t{esc_xml(item)}') - lines.append(f'{indent}\t') - elif item.get('folder'): - lines.append(f'{indent}\t') - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t\t\tru') - lines.append(f'{indent}\t\t\t\t{esc_xml(str(item["folder"]))}') - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t') - for sub in (item.get('items') or []): - sub_name = str(sub.get('field', sub)) if isinstance(sub, dict) else str(sub) - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t\t{esc_xml(sub_name)}') - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\tAuto') - lines.append(f'{indent}\t') - else: - lines.append(f'{indent}\t') - lines.append(f'{indent}\t\t{esc_xml(str(item["field"]))}') - if item.get('title'): - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t\t\tru') - lines.append(f'{indent}\t\t\t\t{esc_xml(str(item["title"]))}') - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t') + if skip_auto and isinstance(item, str) and item == 'Auto': + continue + emit_selection_item(lines, item, f'{indent}\t') lines.append(f'{indent}') @@ -1962,6 +1967,20 @@ def emit_structure_item(lines, item, indent): lines.append(f'{indent}') + elif item_type == 'nestedObject': + lines.append(f'{indent}') + if item.get('objectID'): + lines.append(f'{indent}\t{esc_xml(str(item["objectID"]))}') + lines.append(f'{indent}\t') + s = item.get('settings') or {} + if s.get('selection'): emit_selection(lines, s['selection'], f'{indent}\t\t') + if s.get('filter'): emit_filter(lines, s['filter'], f'{indent}\t\t') + if s.get('order'): emit_order(lines, s['order'], f'{indent}\t\t') + if s.get('conditionalAppearance'): emit_conditional_appearance(lines, s['conditionalAppearance'], f'{indent}\t\t') + if s.get('outputParameters'): emit_output_parameters(lines, s['outputParameters'], f'{indent}\t\t') + lines.append(f'{indent}\t') + lines.append(f'{indent}') + def emit_settings_variants(lines, defn): variants = defn.get('settingsVariants') diff --git a/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 b/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 index b2d14d25..099d8610 100644 --- a/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 +++ b/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 @@ -1,4 +1,4 @@ -# skd-decompile v0.12 — Decompile 1C DCS Template.xml to JSON DSL (draft) +# skd-decompile v0.13 — Decompile 1C DCS Template.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -1028,42 +1028,51 @@ function Build-FilterItem { return $s } +# Recursive helper для одного элемента selection. Возвращает либо строку (имя поля / "Auto"), +# либо ordered hashtable ({field, title} / {folder, items: [...]} / sentinel). +function Build-SelectionItem { + param($item, [string]$loc) + $xt = Get-LocalXsiType $item + # Implicit SelectedItemField: без xsi:type, но с + if (-not $xt) { + $fName = Get-Text $item "dcsset:field" + if ($fName) { return $fName } + } + switch ($xt) { + 'SelectedItemAuto' { return 'Auto' } + 'SelectedItemField' { + $fName = Get-Text $item "dcsset:field" + $titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns) + $title = Get-MLText $titleNode + if ($title) { return [ordered]@{ field = $fName; title = $title } } + return $fName + } + 'SelectedItemFolder' { + $titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns) + $folderTitle = Get-MLText $titleNode + $inner = @() + foreach ($sub in $item.SelectNodes("dcsset:item", $ns)) { + $inner += (Build-SelectionItem -item $sub -loc "$loc/folder") + } + $entry = [ordered]@{ folder = $folderTitle; items = $inner } + # folder может также иметь свой (редко, но встречается) + $folderField = Get-Text $item "dcsset:field" + if ($folderField) { $entry['field'] = $folderField } + return $entry + } + default { + return (New-Sentinel -kind "SelectionItem:$xt" -loc $loc -detail 'Неизвестный тип элемента selection') + } + } +} + # Build selection items array function Build-Selection { param($selNode, [string]$loc) if (-not $selNode) { return @() } $out = @() foreach ($it in $selNode.SelectNodes("dcsset:item", $ns)) { - $xt = Get-LocalXsiType $it - # Implicit SelectedItemField: without xsi:type but with child. - # Платформа эмитит это в conditionalAppearance/selection. - if (-not $xt) { - $fName = Get-Text $it "dcsset:field" - if ($fName) { $out += $fName; continue } - } - switch ($xt) { - 'SelectedItemAuto' { $out += 'Auto' } - 'SelectedItemField' { $out += (Get-Text $it "dcsset:field") } - 'SelectedItemFolder' { - $titleNode = $it.SelectSingleNode("dcsset:lwsTitle", $ns) - $folderTitle = Get-MLText $titleNode - $inner = @() - foreach ($sub in $it.SelectNodes("dcsset:item", $ns)) { - $st = Get-LocalXsiType $sub - if (-not $st) { - $subF = Get-Text $sub "dcsset:field" - if ($subF) { $inner += $subF; continue } - } - if ($st -eq 'SelectedItemField') { $inner += (Get-Text $sub "dcsset:field") } - elseif ($st -eq 'SelectedItemAuto') { $inner += 'Auto' } - else { $inner += (New-Sentinel -kind "SelectionInFolder:$st" -loc "$loc/folder" -detail 'Неизвестный тип элемента папки выбора') } - } - $out += [ordered]@{ folder = $folderTitle; items = $inner } - } - default { - $out += (New-Sentinel -kind "SelectionItem:$xt" -loc $loc -detail 'Неизвестный тип элемента selection') - } - } + $out += (Build-SelectionItem -item $it -loc $loc) } return ,$out } @@ -1219,7 +1228,8 @@ function Build-DataParameters { return ,$entries } -# Read groupItems -> array of field names (with periodAddition/groupType warnings) +# Read groupItems → array. Простые поля → string. С нестандартным groupType/periodAdditionType +# → object form {field, groupType?, periodAdditionType?} (compile принимает оба варианта). function Get-GroupFields { param($parentNode, [string]$loc) $gFields = @() @@ -1231,10 +1241,15 @@ function Get-GroupFields { $gf = Get-Text $gItem "dcsset:field" $pat = Get-Text $gItem "dcsset:periodAdditionType" $gt = Get-Text $gItem "dcsset:groupType" - if (($pat -and $pat -ne 'None') -or ($gt -and $gt -ne 'Items')) { - $null = Add-Warning -kind 'GroupItemDetails' -loc "$loc/groupItems" -detail "Группировка $gf использует groupType=$gt, periodAdditionType=$pat — не воспроизводится в shorthand" + $isDefault = (-not $pat -or $pat -eq 'None') -and (-not $gt -or $gt -eq 'Items') + if ($isDefault) { + $gFields += $gf + } else { + $obj = [ordered]@{ field = $gf } + if ($gt -and $gt -ne 'Items') { $obj['groupType'] = $gt } + if ($pat -and $pat -ne 'None') { $obj['periodAdditionType'] = $pat } + $gFields += $obj } - $gFields += $gf } else { $gFields += (New-Sentinel -kind "GroupItem:$gxt" -loc "$loc/groupItems" -detail 'Тип элемента группировки не покрыт') } @@ -1293,6 +1308,39 @@ function Build-Structure { $idx++ continue } + if ($xt -eq 'StructureItemNestedObject') { + $entry = [ordered]@{ type = 'nestedObject' } + $objID = Get-Text $it "dcsset:objectID" + if ($objID) { $entry['objectID'] = $objID } + $settingsNode = $it.SelectSingleNode("dcsset:settings", $ns) + if ($settingsNode) { + $nestedSettings = [ordered]@{} + $selNode = $settingsNode.SelectSingleNode("dcsset:selection", $ns) + $selI = Build-Selection -selNode $selNode -loc "$loc/$idx/nested/selection" + if ($selI.Count -gt 0) { $nestedSettings['selection'] = $selI } + $fNode = $settingsNode.SelectSingleNode("dcsset:filter", $ns) + if ($fNode -and $fNode.SelectNodes("dcsset:item", $ns).Count -gt 0) { + $fa = @() + foreach ($fc in $fNode.SelectNodes("dcsset:item", $ns)) { $fa += (Build-FilterItem -itemNode $fc -loc "$loc/$idx/nested/filter") } + $nestedSettings['filter'] = $fa + } + $oNode = $settingsNode.SelectSingleNode("dcsset:order", $ns) + $oI = Build-Order -ordNode $oNode -loc "$loc/$idx/nested/order" + if ($oI.Count -gt 0) { $nestedSettings['order'] = $oI } + $caNode = $settingsNode.SelectSingleNode("dcsset:conditionalAppearance", $ns) + if ($caNode) { + $ca = Build-ConditionalAppearance -caNode $caNode -loc "$loc/$idx/nested/ca" + if ($ca.Count -gt 0) { $nestedSettings['conditionalAppearance'] = $ca } + } + $opNode = $settingsNode.SelectSingleNode("dcsset:outputParameters", $ns) + $op = Build-OutputParameters -opNode $opNode + if ($op -and $op.Count -gt 0) { $nestedSettings['outputParameters'] = $op } + $entry['settings'] = $nestedSettings + } + $items += $entry + $idx++ + continue + } if ($xt -eq 'StructureItemChart') { $entry = [ordered]@{ type = 'chart' } $nm = Get-Text $it "dcsset:name" @@ -1322,28 +1370,8 @@ function Build-Structure { # Optional name $nm = Get-Text $it "dcsset:name" if ($nm) { $entry['name'] = $nm } - # groupItems → groupFields - $gi = $it.SelectSingleNode("dcsset:groupItems", $ns) - $gFields = @() - if ($gi) { - foreach ($gItem in $gi.SelectNodes("dcsset:item", $ns)) { - $gxt = Get-LocalXsiType $gItem - if ($gxt -eq 'GroupItemField') { - $gf = Get-Text $gItem "dcsset:field" - # Look at periodAdditionType — non-None or non-default → sentinel - $pat = Get-Text $gItem "dcsset:periodAdditionType" - $gt = Get-Text $gItem "dcsset:groupType" - if (($pat -and $pat -ne 'None') -or ($gt -and $gt -ne 'Items')) { - # Non-default grouping — record but don't fail - # We still emit the field; flag in warnings only - $null = Add-Warning -kind 'GroupItemDetails' -loc "$loc/groupItems" -detail "Группировка $gf использует groupType=$gt, periodAdditionType=$pat — не воспроизводится в shorthand" - } - $gFields += $gf - } else { - $gFields += (New-Sentinel -kind "GroupItem:$gxt" -loc "$loc/groupItems" -detail 'Тип элемента группировки не покрыт') - } - } - } + # groupItems → groupFields (через общий Get-GroupFields с object form поддержкой) + $gFields = Get-GroupFields -parentNode $it -loc $loc if ($gFields.Count -gt 0) { $entry['groupFields'] = $gFields } # Local selection — only emit if not "[Auto]" default @@ -1398,6 +1426,8 @@ function Try-StructureShorthand { break } if ($gfs.Count -ne 1) { return $null } + # Только простые имена-строки сворачиваем в shorthand + if ($gfs[0] -isnot [string]) { return $null } $parts += $gfs[0] $children = $cur['children'] if ($null -eq $children -or $children.Count -eq 0) { break } diff --git a/tests/skills/cases/skd-decompile/snapshots/structure-nested-and-folder/Template.xml b/tests/skills/cases/skd-decompile/snapshots/structure-nested-and-folder/Template.xml new file mode 100644 index 00000000..b3d71cb1 --- /dev/null +++ b/tests/skills/cases/skd-decompile/snapshots/structure-nested-and-folder/Template.xml @@ -0,0 +1,159 @@ + + + + ИсточникДанных1 + Local + + + DSОбъединение + + Период + Период + + xs:dateTime + + Date + + + + + ВидРасчета + ВидРасчета + + d5p1:CatalogRef.ВидыРасчета + + + + Сумма + Сумма + + xs:decimal + + 15 + 2 + Any + + + + + Подразделение + Подразделение + + d5p1:CatalogRef.Подразделения + + + + Часть1 + ИсточникДанных1 + ДанныеЧасть1 + + + Часть2 + ИсточникДанных1 + ДанныеЧасть2 + + + + Основной + + + ru + Основной + + + + + + Период + + + Сумма + + + ru + Итого + + + + + + + ru + Группа итогов + + + + ВидРасчета + + + Сумма + + + ru + Сумма с расшифровкой + + + + + + + ru + Подгруппа + + + + Подразделение + + Auto + + Auto + + + + Группа1 + + + Период + Items + Day + 0001-01-01T00:00:00 + 0001-01-01T00:00:00 + + + Подразделение + Hierarchy + None + 0001-01-01T00:00:00 + 0001-01-01T00:00:00 + + + + + + + + + + ДанныеЧасть1 + + + + ВидРасчета + + + Сумма + + + + + + + + diff --git a/tests/skills/cases/skd-decompile/snapshots/structure-nested-and-folder/decompiled.json b/tests/skills/cases/skd-decompile/snapshots/structure-nested-and-folder/decompiled.json new file mode 100644 index 00000000..c98f2ed9 --- /dev/null +++ b/tests/skills/cases/skd-decompile/snapshots/structure-nested-and-folder/decompiled.json @@ -0,0 +1,80 @@ +{ + "dataSets": [ + { + "name": "DSОбъединение", + "items": [ + { + "name": "Часть1", + "objectName": "ДанныеЧасть1" + }, + { + "name": "Часть2", + "objectName": "ДанныеЧасть2" + } + ], + "fields": [ + "Период: date", + "ВидРасчета: CatalogRef.ВидыРасчета", + "Сумма: decimal(15,2)", + "Подразделение: CatalogRef.Подразделения" + ] + } + ], + "settingsVariants": [ + { + "name": "Основной", + "settings": { + "selection": [ + "Период", + { + "field": "Сумма", + "title": "Итого" + }, + { + "folder": "Группа итогов", + "items": [ + "ВидРасчета", + { + "field": "Сумма", + "title": "Сумма с расшифровкой" + }, + { + "folder": "Подгруппа", + "items": [ + "Подразделение" + ] + } + ] + } + ], + "structure": [ + { + "name": "Группа1", + "groupFields": [ + { + "field": "Период", + "periodAdditionType": "Day" + }, + { + "field": "Подразделение", + "groupType": "Hierarchy" + } + ], + "children": [ + { + "type": "nestedObject", + "objectID": "ДанныеЧасть1", + "settings": { + "selection": [ + "ВидРасчета", + "Сумма" + ] + } + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/skills/cases/skd-decompile/structure-nested-and-folder.json b/tests/skills/cases/skd-decompile/structure-nested-and-folder.json new file mode 100644 index 00000000..a3da1cef --- /dev/null +++ b/tests/skills/cases/skd-decompile/structure-nested-and-folder.json @@ -0,0 +1,65 @@ +{ + "name": "Структура: nestedObject + selection с вложенными folder и field-with-title + groupItem object form", + "preRun": [ + { + "script": "skd-compile/scripts/skd-compile", + "input": { + "dataSets": [ + { + "name": "DSОбъединение", + "items": [ + { "name": "Часть1", "objectName": "ДанныеЧасть1" }, + { "name": "Часть2", "objectName": "ДанныеЧасть2" } + ], + "fields": [ + "Период: date", + "ВидРасчета: CatalogRef.ВидыРасчета", + "Сумма: decimal(15,2)", + "Подразделение: CatalogRef.Подразделения" + ] + } + ], + "settingsVariants": [ + { + "name": "Основной", + "settings": { + "selection": [ + "Период", + { "field": "Сумма", "title": "Итого" }, + { + "folder": "Группа итогов", + "items": [ + "ВидРасчета", + { "field": "Сумма", "title": "Сумма с расшифровкой" }, + { + "folder": "Подгруппа", + "items": ["Подразделение"] + } + ] + } + ], + "structure": [ + { + "name": "Группа1", + "groupFields": [ + { "field": "Период", "periodAdditionType": "Day" }, + { "field": "Подразделение", "groupType": "Hierarchy" } + ], + "children": [ + { "type": "nestedObject", "objectID": "ДанныеЧасть1", "settings": { + "selection": ["ВидРасчета", "Сумма"] + }} + ] + } + ] + } + } + ] + }, + "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "Template.xml" }, + "cwd": "{workDir}" + } + ], + "params": { "templatePath": "Template.xml" }, + "outputPath": "decompiled.json" +}