From 5ec21f24b44bb1aafa6a1a2fa2b0ad312334581f Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 21 May 2026 13:40:46 +0300 Subject: [PATCH] =?UTF-8?q?feat(skd-decompile):=20scaffold=20=E2=80=94=20R?= =?UTF-8?q?ing=203=20fail-fast,=20sentinel/warnings,=20query=20extraction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layer 1 of the skd-decompile plan: SKILL.md with disable-model-invocation, ps1 skeleton with XML→JSON pipeline, namespace probe for non-DCS root, sentinel/warnings accumulator, and DataSetQuery extraction (query only). Test case minimal-query demonstrates round-trip via skd-compile preRun. Co-Authored-By: Claude Opus 4.7 --- .claude/skills/skd-decompile/SKILL.md | 64 +++++++ .../skd-decompile/scripts/skd-decompile.ps1 | 159 ++++++++++++++++++ tests/skills/cases/skd-decompile/_skill.json | 11 ++ .../cases/skd-decompile/minimal-query.json | 19 +++ 4 files changed, 253 insertions(+) create mode 100644 .claude/skills/skd-decompile/SKILL.md create mode 100644 .claude/skills/skd-decompile/scripts/skd-decompile.ps1 create mode 100644 tests/skills/cases/skd-decompile/_skill.json create mode 100644 tests/skills/cases/skd-decompile/minimal-query.json diff --git a/.claude/skills/skd-decompile/SKILL.md b/.claude/skills/skd-decompile/SKILL.md new file mode 100644 index 00000000..47f55aeb --- /dev/null +++ b/.claude/skills/skd-decompile/SKILL.md @@ -0,0 +1,64 @@ +--- +name: skd-decompile +description: Декомпиляция схемы компоновки данных 1С (СКД) в JSON-черновик в формате skd-compile. Используй когда нужно создать новый отчёт по образцу существующего или провести структурный рефакторинг. Для точечных правок используй skd-edit +argument-hint: [-OutputPath ] +disable-model-invocation: true +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /skd-decompile — извлечение JSON-черновика из Template.xml СКД + +Читает существующий `Template.xml` (DataCompositionSchema) и эмитит JSON в формате, который принимает `/skd-compile`. Получившийся JSON — **черновик**: гарантируется только структурная эквивалентность, не байтовая. + +## Когда использовать + +- **Scaffold нового отчёта по образцу.** Взять существующий СКД, получить JSON, поправить параметры/поля/шаблоны, скомпилировать в новый отчёт. +- **Глобальный рефакторинг.** Когда правка структурная (переписать вариант, перерисовать шаблон), а не точечная. + +## Когда **не** использовать + +- **Точечные правки готового отчёта** — добавить поле, фильтр, итог, переименовать. Для этого есть `/skd-edit`: точечно, без полной реконструкции, без риска потерь. +- **Анализ схемы** — для обзора используй `/skd-info` (overview/query/fields/variant/templates). + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `TemplatePath` | Путь к Template.xml (обязательный) | +| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout | + +```powershell +# В файл (рядом, если есть warnings, кладётся .warnings.md) +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-decompile.ps1" -TemplatePath "" -OutputPath "" + +# В stdout +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-decompile.ps1" -TemplatePath "" +``` + +## Гарантии и ограничения + +- **JSON всегда валиден** — компилируется через `skd-compile` без синтаксических ошибок. +- **Покрытие — DSL `skd-compile`.** Конструкции XML вне DSL отмечаются sentinel-объектом `{"__unsupported__": {...}}` и описаны в `.warnings.md` рядом с JSON. `skd-compile` фейлится при наличии sentinel — это специально, чтобы пользователь сначала разобрался с непокрытым. +- **Не байтовая эквивалентность.** После round-trip XML структурно эквивалентен оригиналу, но порядок атрибутов/секций может отличаться. +- **Стиль ячейки** определяется по совпадению с одним из built-in (`header`/`data`/`subheader`/`total`) или user-стилей из `presets/skills/skd/skd-styles.json`. Точечный custom appearance не сворачивается → sentinel. + +## Не поддерживается (fail-fast) + +- Picture cells в шаблонах (``). +- Параметры типа ХранилищеЗначения. +- Sibling templates / templateCondition (вариативные шаблоны). +- Не-СКД корневые XML (например, spreadsheet `` — для них есть `/mxl-decompile`). + +При обнаружении — скрипт пишет в stderr понятное сообщение и завершается с ненулевым кодом. + +## Верификация + +``` +/skd-compile -DefinitionFile -OutputPath — обратная компиляция +/skd-validate — валидация результата +/skd-info — визуальный осмотр +``` diff --git a/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 b/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 new file mode 100644 index 00000000..3b2162ee --- /dev/null +++ b/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 @@ -0,0 +1,159 @@ +# skd-decompile v0.1 — Decompile 1C DCS Template.xml to JSON DSL (draft) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$TemplatePath, + + [string]$OutputPath +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- 0. Resolve and validate input --- + +if (-not (Test-Path $TemplatePath)) { + Write-Error "Template not found: $TemplatePath" + exit 1 +} + +$TemplatePath = (Resolve-Path $TemplatePath).Path + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $false +$xmlDoc.Load($TemplatePath) + +$root = $xmlDoc.DocumentElement + +# Ring 3: not a DataCompositionSchema → fail-fast +if ($root.LocalName -ne 'DataCompositionSchema') { + Write-Error "Root element <$($root.LocalName)> is not . This is not a SKD template (perhaps a spreadsheet — use /mxl-decompile)." + exit 2 +} + +# --- 1. Namespace manager --- + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("dcs", "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("dcsat", "http://v8.1c.ru/8.1/data-composition-system/areatemplate") +$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 may use default namespace = schema; XPath needs explicit prefix +$rootNS = $root.NamespaceURI +if (-not $rootNS) { $rootNS = "http://v8.1c.ru/8.1/data-composition-system/schema" } +# Re-bind dcs to actual root namespace if different +$ns.AddNamespace("r", $rootNS) + +# --- 2. Warnings accumulator --- + +$script:warnings = @() +$script:warningCounter = 0 + +function Add-Warning { + param([string]$kind, [string]$loc, [string]$detail) + $script:warningCounter++ + $id = "W{0:D3}" -f $script:warningCounter + $script:warnings += [ordered]@{ id = $id; kind = $kind; loc = $loc; detail = $detail } + return $id +} + +function New-Sentinel { + param([string]$kind, [string]$loc, [string]$detail) + $id = Add-Warning -kind $kind -loc $loc -detail $detail + return [ordered]@{ '__unsupported__' = [ordered]@{ id = $id; kind = $kind; loc = $loc } } +} + +# --- 3. Extract: dataSets (query only, no fields yet) --- + +function Get-Text { + param($node, [string]$xpath) + if (-not $node) { return $null } + $n = $node.SelectSingleNode($xpath, $ns) + if ($n) { return $n.InnerText } else { return $null } +} + +$dataSets = @() +$dsNodes = $root.SelectNodes("r:dataSet", $ns) +foreach ($dsNode in $dsNodes) { + $xsiType = $dsNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + $name = Get-Text $dsNode "r:name" + $ds = [ordered]@{ name = $name } + + switch -Regex ($xsiType) { + 'DataSetQuery$' { + $query = Get-Text $dsNode "r:query" + # Decode XML entities — XmlDocument already decoded & → & in InnerText + $ds['query'] = $query + # fields — layer 3 + } + 'DataSetObject$' { + $ds['objectName'] = Get-Text $dsNode "r:objectName" + } + 'DataSetUnion$' { + $ds['__unsupported__'] = (New-Sentinel -kind 'DataSetUnion' -loc "dataSet[name=$name]" -detail 'Реализуется в слое 15')['__unsupported__'] + } + default { + $ds['__unsupported__'] = (New-Sentinel -kind "DataSetType:$xsiType" -loc "dataSet[name=$name]" -detail "Неизвестный тип набора данных")['__unsupported__'] + } + } + + $dataSets += $ds +} + +# --- 4. Build top-level JSON object --- + +$out = [ordered]@{ + dataSets = $dataSets +} + +# --- 5. Serialize --- + +$json = $out | ConvertTo-Json -Depth 32 + +# Unescape \uXXXX → UTF-8 literals (PS 5.1 ConvertTo-Json escapes non-ASCII) +$json = [regex]::Replace($json, '\\u([0-9a-fA-F]{4})', { + param($m) + [char][int]("0x" + $m.Groups[1].Value) +}) + +if ($OutputPath) { + if (-not [System.IO.Path]::IsPathRooted($OutputPath)) { + $OutputPath = Join-Path (Get-Location).Path $OutputPath + } + $enc = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($OutputPath, $json, $enc) + + # Write warnings.md alongside, if any + if ($script:warnings.Count -gt 0) { + $wPath = [System.IO.Path]::ChangeExtension($OutputPath, $null).TrimEnd('.') + '.warnings.md' + $sb = New-Object System.Text.StringBuilder + [void]$sb.AppendLine("# skd-decompile warnings") + [void]$sb.AppendLine("") + [void]$sb.AppendLine("Source: $TemplatePath") + [void]$sb.AppendLine("") + foreach ($w in $script:warnings) { + [void]$sb.AppendLine("- **$($w.id)** ($($w.kind)) at `$($w.loc)`: $($w.detail)") + } + [System.IO.File]::WriteAllText($wPath, $sb.ToString(), $enc) + Write-Host "Warnings written: $wPath ($($script:warnings.Count) issue(s))" -ForegroundColor Yellow + } + + # Summary to stderr + $stats = "dataSets=$($dataSets.Count), warnings=$($script:warnings.Count)" + [Console]::Error.WriteLine("Decompiled: $stats") +} else { + Write-Output $json + if ($script:warnings.Count -gt 0) { + [Console]::Error.WriteLine("Warnings ($($script:warnings.Count)):") + foreach ($w in $script:warnings) { + [Console]::Error.WriteLine(" $($w.id) [$($w.kind)] $($w.loc): $($w.detail)") + } + } +} diff --git a/tests/skills/cases/skd-decompile/_skill.json b/tests/skills/cases/skd-decompile/_skill.json new file mode 100644 index 00000000..f8cbe464 --- /dev/null +++ b/tests/skills/cases/skd-decompile/_skill.json @@ -0,0 +1,11 @@ +{ + "script": "skd-decompile/scripts/skd-decompile", + "setup": "none", + "args": [ + { "flag": "-TemplatePath", "from": "workPath", "field": "templatePath" } + ], + "snapshot": { + "root": "workDir", + "normalizeUuids": false + } +} diff --git a/tests/skills/cases/skd-decompile/minimal-query.json b/tests/skills/cases/skd-decompile/minimal-query.json new file mode 100644 index 00000000..2c400d28 --- /dev/null +++ b/tests/skills/cases/skd-decompile/minimal-query.json @@ -0,0 +1,19 @@ +{ + "name": "Минимальный СКД с одним DataSetQuery", + "preRun": [ + { + "script": "skd-compile/scripts/skd-compile", + "input": { + "dataSets": [{ + "name": "НаборДанных1", + "query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура", + "fields": ["Наименование"] + }] + }, + "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "Template.xml" }, + "cwd": "{workDir}" + } + ], + "params": { "templatePath": "Template.xml" }, + "expect": { "stdoutContains": "НаборДанных1" } +}