From 5ec21f24b44bb1aafa6a1a2fa2b0ad312334581f Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 21 May 2026 13:40:46 +0300 Subject: [PATCH 001/138] =?UTF-8?q?feat(skd-decompile):=20scaffold=20?= =?UTF-8?q?=E2=80=94=20Ring=203=20fail-fast,=20sentinel/warnings,=20query?= =?UTF-8?q?=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" } +} From 643211f2fb74d1a2f95b6509ccde64d9d206ec5b Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 21 May 2026 13:50:46 +0300 Subject: [PATCH 002/138] =?UTF-8?q?docs(skd-decompile):=20=D1=87=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BD=D0=B0=D1=8F=20=D1=84=D0=BE=D1=80=D0=BC=D1=83?= =?UTF-8?q?=D0=BB=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20scope=20=D0=B2=20S?= =?UTF-8?q?KILL.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Убрал ложное обещание «структурной эквивалентности» (DSL покрывает подмножество СКД). Слил «Гарантии» и «Не поддерживается» в раздел «Что получаешь» с тремя категориями (покрытое / sentinel / fail-fast). Добавил Workflow — декомпил это начало процесса, а не финал. Co-Authored-By: Claude Opus 4.7 --- .claude/skills/skd-decompile/SKILL.md | 47 +++++++++++---------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/.claude/skills/skd-decompile/SKILL.md b/.claude/skills/skd-decompile/SKILL.md index 47f55aeb..8b5e1277 100644 --- a/.claude/skills/skd-decompile/SKILL.md +++ b/.claude/skills/skd-decompile/SKILL.md @@ -10,21 +10,20 @@ allowed-tools: - Glob --- -# /skd-decompile — извлечение JSON-черновика из Template.xml СКД +# /skd-decompile — JSON-черновик из Template.xml СКД -Читает существующий `Template.xml` (DataCompositionSchema) и эмитит JSON в формате, который принимает `/skd-compile`. Получившийся JSON — **черновик**: гарантируется только структурная эквивалентность, не байтовая. +Читает Template.xml и эмитит JSON в формате `skd-compile`. **Результат — черновик**, а не обратимое представление: см. раздел «Что получаешь». ## Когда использовать -- **Scaffold нового отчёта по образцу.** Взять существующий СКД, получить JSON, поправить параметры/поля/шаблоны, скомпилировать в новый отчёт. -- **Глобальный рефакторинг.** Когда правка структурная (переписать вариант, перерисовать шаблон), а не точечная. +- **Scaffold нового отчёта по образцу** — взять существующий СКД, получить JSON, поправить и скомпилировать в новый. +- **Структурный рефакторинг** — переписать вариант, перерисовать шаблон, перебрать набор полей. ## Когда **не** использовать -- **Точечные правки готового отчёта** — добавить поле, фильтр, итог, переименовать. Для этого есть `/skd-edit`: точечно, без полной реконструкции, без риска потерь. -- **Анализ схемы** — для обзора используй `/skd-info` (overview/query/fields/variant/templates). +- **Точечные правки готового отчёта** (добавить поле, фильтр, итог, переименовать) → `/skd-edit`. Точечно, без потерь, без полной реконструкции. -## Параметры и команда +## Параметры | Параметр | Описание | |----------|----------| @@ -32,33 +31,25 @@ allowed-tools: | `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 "" ``` -## Гарантии и ограничения +При наличии `-OutputPath` рядом пишется `.warnings.md`, если есть непокрытые конструкции. -- **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) +JSON — это **черновик, не полное обратимое представление СКД**. Декомпилятор знает только то, что умеет `skd-compile`, поэтому: -- Picture cells в шаблонах (``). -- Параметры типа ХранилищеЗначения. -- Sibling templates / templateCondition (вариативные шаблоны). -- Не-СКД корневые XML (например, spreadsheet `` — для них есть `/mxl-decompile`). +- **Покрытые конструкции** эмитятся в JSON напрямую (поля, параметры с `@autoDates`, шаблоны с rows-стилями, варианты с structure/selection/filter/order/conditionalAppearance и т.п.). +- **Непокрытые, но не критичные** (например, `orderExpression` на полях, `ChoiceParameterLinks` на параметрах, custom per-cell appearance, scope в conditionalAppearance) — заменяются на sentinel `{"__unsupported__": {"id": "W###", "kind": "...", "loc": "..."}}`. JSON остаётся валидным, но **`skd-compile` отказывается компилировать его до тех пор, пока sentinel не убраны** — это намеренно, чтобы непокрытое не уехало в финальный отчёт незамеченным. +- **Критичные конструкции** (Picture cells, ХранилищеЗначения, вариативные шаблоны, не-СКД root) — fail-fast: скрипт завершается с ненулевым кодом и пишет в stderr какой именно элемент не поддержан. -При обнаружении — скрипт пишет в stderr понятное сообщение и завершается с ненулевым кодом. +Все непокрытые места — с координатами в `.warnings.md`. -## Верификация +## Workflow -``` -/skd-compile -DefinitionFile -OutputPath — обратная компиляция -/skd-validate — валидация результата -/skd-info — визуальный осмотр -``` +1. `/skd-decompile -OutputPath draft.json` — получить черновик. +2. Открыть `draft.warnings.md`, посмотреть, что не покрылось. +3. Поправить JSON под задачу. Sentinel-объекты — заменить на ручную реализацию (через явный raw `template`, через ручное описание appearance и т.п.) либо удалить, если конструкция в новом отчёте не нужна. +4. `/skd-compile -DefinitionFile draft.json -OutputPath new-Template.xml` — собрать обратно. +5. `/skd-validate` + `/skd-info` — проверить. From 765e1d88850b0dd16d35e689aa77d37995b41f8e Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 21 May 2026 13:54:17 +0300 Subject: [PATCH 003/138] =?UTF-8?q?feat(skd-decompile):=20=D1=81=D0=BB?= =?UTF-8?q?=D0=BE=D0=B8=202-3=20=E2=80=94=20dataSources,=20dataSets,=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Распознавание типов (string/decimal/boolean/date/dateTime/time/CatalogRef.X и пр.) с qualifiers (decimal-точность/знак, string Length/AllowedLength, date fractions) → shorthand или composite-массив. - Роли (@dimension/@account/@balance/@period) с детектом сложных roleAttributes как sentinel. - Restrictions (#noField/#noFilter/#noGroup/#noOrder) из useRestriction. - Multilingual title с авто-сворачиванием {ru:"..."} в строку. - appearance с поддержкой LocalStringType значений (например, Формат). - presentationExpression. - Свёртка дефолтного dataSource (ИсточникДанных1/Local) в умолчание. - Автодетект object vs shorthand формы поля. Bit-perfect round-trip на синтетике из 11 разнотипных полей. Реальный ERP-отчёт АнализВерсийОбъектов декомпилируется с 0 warnings. Co-Authored-By: Claude Opus 4.7 --- .../skd-decompile/scripts/skd-decompile.ps1 | 325 +++++++++++++++--- 1 file changed, 286 insertions(+), 39 deletions(-) diff --git a/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 b/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 index 3b2162ee..b37097a2 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.1 — Decompile 1C DCS Template.xml to JSON DSL (draft) +# skd-decompile v0.2 — Decompile 1C DCS Template.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -34,22 +34,27 @@ if ($root.LocalName -ne 'DataCompositionSchema') { # --- 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") +$NS_SCHEMA = "http://v8.1c.ru/8.1/data-composition-system/schema" +$NS_COM = "http://v8.1c.ru/8.1/data-composition-system/common" +$NS_COR = "http://v8.1c.ru/8.1/data-composition-system/core" +$NS_SET = "http://v8.1c.ru/8.1/data-composition-system/settings" +$NS_AT = "http://v8.1c.ru/8.1/data-composition-system/areatemplate" +$NS_V8 = "http://v8.1c.ru/8.1/data/core" +$NS_V8UI = "http://v8.1c.ru/8.1/data/ui" +$NS_XS = "http://www.w3.org/2001/XMLSchema" +$NS_XSI = "http://www.w3.org/2001/XMLSchema-instance" +$NS_CFG = "http://v8.1c.ru/8.1/data/enterprise/current-config" -# 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) +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("r", $NS_SCHEMA) +$ns.AddNamespace("dcscom", $NS_COM) +$ns.AddNamespace("dcscor", $NS_COR) +$ns.AddNamespace("dcsset", $NS_SET) +$ns.AddNamespace("dcsat", $NS_AT) +$ns.AddNamespace("v8", $NS_V8) +$ns.AddNamespace("v8ui", $NS_V8UI) +$ns.AddNamespace("xs", $NS_XS) +$ns.AddNamespace("xsi", $NS_XSI) # --- 2. Warnings accumulator --- @@ -70,7 +75,7 @@ function New-Sentinel { return [ordered]@{ '__unsupported__' = [ordered]@{ id = $id; kind = $kind; loc = $loc } } } -# --- 3. Extract: dataSets (query only, no fields yet) --- +# --- 3. Helpers --- function Get-Text { param($node, [string]$xpath) @@ -79,45 +84,290 @@ function Get-Text { if ($n) { return $n.InnerText } else { return $null } } +# Extract LocalStringType (multilingual title) → string (if only ru) or hashtable +function Get-MLText { + param($node) + if (-not $node) { return $null } + $items = $node.SelectNodes("v8:item", $ns) + if ($items.Count -eq 0) { return $null } + $dict = [ordered]@{} + foreach ($it in $items) { + $lang = Get-Text $it "v8:lang" + $content = Get-Text $it "v8:content" + if ($lang) { $dict[$lang] = if ($content) { $content } else { "" } } + } + if ($dict.Count -eq 1 -and $dict.Contains('ru')) { return $dict['ru'] } + return $dict +} + +# Strip namespace prefix from xsi:type value (e.g. "dcsset:Foo" → "Foo") +function Get-LocalXsiType { + param($node) + if (-not $node) { return $null } + $t = $node.GetAttribute("type", $NS_XSI) + if ($t -match ':(.+)$') { return $matches[1] } + return $t +} + +# Convert one element + sibling qualifiers → shorthand type string +function Get-OneTypeShorthand { + param($typeNode, $qualNumber, $qualString, $qualDate) + $raw = $typeNode.InnerText.Trim() + # Strip namespace prefix; check if it's d5p1: (config refs) + $local = $raw + if ($raw -match '^([^:]+):(.+)$') { + $prefix = $matches[1] + $local = $matches[2] + # Resolve prefix → namespace URI + $uri = $typeNode.GetNamespaceOfPrefix($prefix) + if ($uri -eq $NS_CFG) { + return $local # CatalogRef.X, DocumentRef.X, etc. + } + if ($uri -eq $NS_XS) { + switch ($local) { + 'string' { + if ($qualString) { + $len = [int](Get-Text $qualString "v8:Length") + $allowed = Get-Text $qualString "v8:AllowedLength" + if ($len -eq 0) { return 'string' } + if ($allowed -eq 'Fixed') { return "string($len,fix)" } + return "string($len)" + } + return 'string' + } + 'boolean' { return 'boolean' } + 'decimal' { + if ($qualNumber) { + $d = [int](Get-Text $qualNumber "v8:Digits") + $f = [int](Get-Text $qualNumber "v8:FractionDigits") + $sign = Get-Text $qualNumber "v8:AllowedSign" + $signSuf = '' + if ($sign -eq 'Nonnegative') { $signSuf = ',nonneg' } + # defaults: 10,2,Any → "decimal" + if ($d -eq 10 -and $f -eq 2 -and -not $signSuf) { return 'decimal' } + if ($f -eq 0) { return "decimal($d$signSuf)" } + return "decimal($d,$f$($signSuf -replace '^,',''))".Replace('decimal(', 'decimal(').Replace(',,',',') + } + return 'decimal' + } + 'dateTime' { + $frac = if ($qualDate) { Get-Text $qualDate "v8:DateFractions" } else { 'DateTime' } + switch ($frac) { + 'Date' { return 'date' } + 'Time' { return 'time' } + default { return 'dateTime' } + } + } + default { return $local } + } + } + if ($uri -eq $NS_V8) { + # v8:StandardPeriod, etc. + return $local + } + } + return $local +} + +# valueType → string shorthand OR array of shorthands (composite) +function Get-ValueTypeShorthand { + param($valueTypeNode) + if (-not $valueTypeNode) { return $null } + $types = $valueTypeNode.SelectNodes("v8:Type", $ns) + if ($types.Count -eq 0) { return $null } + $qualN = $valueTypeNode.SelectSingleNode("v8:NumberQualifiers", $ns) + $qualS = $valueTypeNode.SelectSingleNode("v8:StringQualifiers", $ns) + $qualD = $valueTypeNode.SelectSingleNode("v8:DateQualifiers", $ns) + $shorts = @() + foreach ($t in $types) { $shorts += (Get-OneTypeShorthand -typeNode $t -qualNumber $qualN -qualString $qualS -qualDate $qualD) } + if ($shorts.Count -eq 1) { return $shorts[0] } + return ,$shorts +} + +# → array of @tokens; if non-simple — null + sentinel via $script:roleSentinel +function Get-RoleTokens { + param($roleNode, [string]$loc) + if (-not $roleNode) { return $null } + $tokens = @() + $hasComplex = $false + foreach ($child in $roleNode.ChildNodes) { + if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } + if ($child.NamespaceURI -ne $NS_COM) { $hasComplex = $true; continue } + switch ($child.LocalName) { + 'dimension' { if ($child.InnerText -eq 'true') { $tokens += '@dimension' } } + 'account' { if ($child.InnerText -eq 'true') { $tokens += '@account' } } + 'balance' { if ($child.InnerText -eq 'true') { $tokens += '@balance' } } + 'periodNumber' { + # Expect periodNumber=1 + periodType=Main → @period + $pType = Get-Text $roleNode "dcscom:periodType" + if ($child.InnerText -eq '1' -and $pType -eq 'Main') { $tokens += '@period' } else { $hasComplex = $true } + } + 'periodType' { } # handled with periodNumber above + default { + $hasComplex = $true + } + } + } + if ($hasComplex) { + # emit sentinel separately so caller can attach it to field obj + $null = New-Sentinel -kind 'ComplexRole' -loc $loc -detail 'Роль с дополнительными атрибутами не сворачивается в @-флаг' + } + return $tokens +} + +# → array of #tokens +function Get-RestrictionTokens { + param($urNode) + if (-not $urNode) { return @() } + $tokens = @() + $map = @{ 'field' = '#noField'; 'condition' = '#noFilter'; 'group' = '#noGroup'; 'order' = '#noOrder' } + foreach ($key in 'field','condition','group','order') { + $v = Get-Text $urNode "r:$key" + if ($v -eq 'true') { $tokens += $map[$key] } + } + return $tokens +} + +# → hashtable {param: value} +function Get-AppearanceDict { + param($appNode) + if (-not $appNode) { return $null } + $dict = [ordered]@{} + $items = $appNode.SelectNodes("dcscor:item", $ns) + foreach ($it in $items) { + $p = Get-Text $it "dcscor:parameter" + $valNode = $it.SelectSingleNode("dcscor:value", $ns) + if (-not $p -or -not $valNode) { continue } + # Value can be xs:string, v8ui:HorizontalAlign, v8:LocalStringType, etc. + $valType = Get-LocalXsiType $valNode + if ($valType -eq 'LocalStringType') { + $dict[$p] = Get-MLText $valNode + } else { + $dict[$p] = $valNode.InnerText + } + } + return $dict +} + +# Build a field JSON entry (shorthand if possible, object form otherwise) +function Build-Field { + param($fieldNode, [string]$loc) + $dataPath = Get-Text $fieldNode "r:dataPath" + $fieldName = Get-Text $fieldNode "r:field" + $titleNode = $fieldNode.SelectSingleNode("r:title", $ns) + $title = Get-MLText $titleNode + $valueTypeNode = $fieldNode.SelectSingleNode("r:valueType", $ns) + $typeShort = Get-ValueTypeShorthand $valueTypeNode + $roleTokens = Get-RoleTokens $fieldNode.SelectSingleNode("r:role", $ns) "$loc/role" + $restrictTokens = Get-RestrictionTokens $fieldNode.SelectSingleNode("r:useRestriction", $ns) + $appNode = $fieldNode.SelectSingleNode("r:appearance", $ns) + $appearance = Get-AppearanceDict $appNode + $presExpr = Get-Text $fieldNode "r:presentationExpression" + + $needsObject = $title -or $appearance -or $presExpr -or ($typeShort -is [array]) + + if (-not $needsObject) { + # shorthand: "Name: type @role #restrict" + $parts = @($fieldName) + if ($typeShort) { $parts[0] = "$fieldName`: $typeShort" } + if ($roleTokens) { $parts[0] += ' ' + ($roleTokens -join ' ') } + if ($restrictTokens) { $parts[0] += ' ' + ($restrictTokens -join ' ') } + # dataPath ≠ field — fall back to object form + if ($dataPath -and $dataPath -ne $fieldName) { + # unusual case; use object form + } else { + return $parts[0] + } + } + + $obj = [ordered]@{ field = $fieldName } + if ($dataPath -and $dataPath -ne $fieldName) { $obj['dataPath'] = $dataPath } + if ($title) { $obj['title'] = $title } + if ($typeShort) { $obj['type'] = $typeShort } + if ($roleTokens) { + if ($roleTokens.Count -eq 1) { $obj['role'] = $roleTokens[0] -replace '^@','' } + else { $obj['role'] = ($roleTokens | ForEach-Object { $_ -replace '^@','' }) } + } + if ($restrictTokens) { $obj['restrict'] = ($restrictTokens | ForEach-Object { $_ -replace '^#','' }) } + if ($presExpr) { $obj['presentationExpression'] = $presExpr } + if ($appearance) { $obj['appearance'] = $appearance } + return $obj +} + +# --- 4. dataSources --- + +$dataSources = @() +$dsourceNodes = $root.SelectNodes("r:dataSource", $ns) +foreach ($dsn in $dsourceNodes) { + $nm = Get-Text $dsn "r:name" + $tp = Get-Text $dsn "r:dataSourceType" + $dataSources += [ordered]@{ name = $nm; type = $tp } +} +# Default: single ИсточникДанных1/Local → omit from output +$emitDataSources = $true +if ($dataSources.Count -eq 1 -and $dataSources[0].name -eq 'ИсточникДанных1' -and $dataSources[0].type -eq 'Local') { + $emitDataSources = $false +} + +# --- 5. dataSets --- + $dataSets = @() $dsNodes = $root.SelectNodes("r:dataSet", $ns) foreach ($dsNode in $dsNodes) { - $xsiType = $dsNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + $xsiType = Get-LocalXsiType $dsNode $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 + switch ($xsiType) { + 'DataSetQuery' { + $ds['query'] = Get-Text $dsNode "r:query" } - 'DataSetObject$' { + 'DataSetObject' { $ds['objectName'] = Get-Text $dsNode "r:objectName" } - 'DataSetUnion$' { - $ds['__unsupported__'] = (New-Sentinel -kind 'DataSetUnion' -loc "dataSet[name=$name]" -detail 'Реализуется в слое 15')['__unsupported__'] + 'DataSetUnion' { + $ds['__unsupported__'] = (New-Sentinel -kind 'DataSetUnion' -loc "dataSet[$name]" -detail 'Реализуется в слое 15')['__unsupported__'] } default { - $ds['__unsupported__'] = (New-Sentinel -kind "DataSetType:$xsiType" -loc "dataSet[name=$name]" -detail "Неизвестный тип набора данных")['__unsupported__'] + $ds['__unsupported__'] = (New-Sentinel -kind "DataSetType:$xsiType" -loc "dataSet[$name]" -detail "Неизвестный тип набора данных")['__unsupported__'] } } + # Fields + $fieldNodes = $dsNode.SelectNodes("r:field", $ns) + if ($fieldNodes.Count -gt 0) { + $fields = @() + $fi = 0 + foreach ($fn in $fieldNodes) { + $fxsi = Get-LocalXsiType $fn + if ($fxsi -ne 'DataSetFieldField') { + $fields += (New-Sentinel -kind "FieldType:$fxsi" -loc "dataSet[$name]/field[$fi]" -detail 'Тип поля не DataSetFieldField') + } else { + $fields += (Build-Field -fieldNode $fn -loc "dataSet[$name]/field[$fi]") + } + $fi++ + } + $ds['fields'] = $fields + } + + # dataSource attachment — omit if matches default + $dsSrc = Get-Text $dsNode "r:dataSource" + if ($emitDataSources -and $dsSrc) { $ds['dataSource'] = $dsSrc } + $dataSets += $ds } -# --- 4. Build top-level JSON object --- +# --- 6. Build top-level JSON object --- -$out = [ordered]@{ - dataSets = $dataSets -} +$out = [ordered]@{} +if ($emitDataSources) { $out['dataSources'] = $dataSources } +$out['dataSets'] = $dataSets -# --- 5. Serialize --- +# --- 7. Serialize --- $json = $out | ConvertTo-Json -Depth 32 -# Unescape \uXXXX → UTF-8 literals (PS 5.1 ConvertTo-Json escapes non-ASCII) +# Unescape \uXXXX → UTF-8 literals $json = [regex]::Replace($json, '\\u([0-9a-fA-F]{4})', { param($m) [char][int]("0x" + $m.Groups[1].Value) @@ -130,7 +380,6 @@ if ($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 @@ -142,12 +391,10 @@ if ($OutputPath) { [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 + Write-Host "Warnings: $wPath ($($script:warnings.Count) issue(s))" -ForegroundColor Yellow } - # Summary to stderr - $stats = "dataSets=$($dataSets.Count), warnings=$($script:warnings.Count)" - [Console]::Error.WriteLine("Decompiled: $stats") + [Console]::Error.WriteLine("Decompiled: dataSets=$($dataSets.Count), warnings=$($script:warnings.Count)") } else { Write-Output $json if ($script:warnings.Count -gt 0) { From be69bc231ca74bdd4f708060181d83431e6fe727 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 21 May 2026 14:41:15 +0300 Subject: [PATCH 004/138] =?UTF-8?q?feat(skd-decompile):=20=D1=81=D0=BB?= =?UTF-8?q?=D0=BE=D0=B8=204-5=20=E2=80=94=20calculatedFields,=20totalField?= =?UTF-8?q?s,=20parameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - calculatedFields: shorthand с [title], type, expression и #restrict-флагами; object form при appearance или multilang title. - totalFields: детект Func(name) и Func(expr) → shorthand "name: Func"/"name: Func(expr)"; object form при привязке к группе. - parameters: - shorthand с [title], type, value, @-флагами; - распознавание StandardPeriod variants → значение в shorthand; - @valueList, @hidden флаги; - availableValues с presentation; - object form для availableValues/multilang/composite type/expression. - autoDates-сворачивание: для каждого StandardPeriod-параметра ищем пару dependent с expression `&P.ДатаНачала`/`&P.ДатаОкончания` (распознаём по expression, не по имени) и сворачиваем в @autoDates на родителе. - decimal-тип всегда эмитится с явными (D,F) — JSON читаемее. - useRestriction суппрессим в параметрах (auto-generated для @hidden). Bit-perfect round-trip 7468→7468 байт на синтетике (3 calc + 2 total + 5 параметров включая @autoDates). Реальный ERP «АнализИзмененийЛичныхДанныхСотрудников» (1035 строк) — 0 warnings при декомпиляции. Co-Authored-By: Claude Opus 4.7 --- .../skd-decompile/scripts/skd-decompile.ps1 | 269 +++++++++++++++++- 1 file changed, 264 insertions(+), 5 deletions(-) diff --git a/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 b/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 index b37097a2..b703243c 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.2 — Decompile 1C DCS Template.xml to JSON DSL (draft) +# skd-decompile v0.3 — Decompile 1C DCS Template.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -143,10 +143,10 @@ function Get-OneTypeShorthand { $sign = Get-Text $qualNumber "v8:AllowedSign" $signSuf = '' if ($sign -eq 'Nonnegative') { $signSuf = ',nonneg' } - # defaults: 10,2,Any → "decimal" - if ($d -eq 10 -and $f -eq 2 -and -not $signSuf) { return 'decimal' } + # Always explicit (D,F) — JSON readable, no surprise from default folding if ($f -eq 0) { return "decimal($d$signSuf)" } - return "decimal($d,$f$($signSuf -replace '^,',''))".Replace('decimal(', 'decimal(').Replace(',,',',') + if ($signSuf) { return "decimal($d,$f$signSuf)" } + return "decimal($d,$f)" } return 'decimal' } @@ -294,6 +294,204 @@ function Build-Field { return $obj } +# Build calculatedField → shorthand string or object form +function Build-CalcField { + param($cfNode, [string]$loc) + $dataPath = Get-Text $cfNode "r:dataPath" + $expression = Get-Text $cfNode "r:expression" + $titleNode = $cfNode.SelectSingleNode("r:title", $ns) + $title = Get-MLText $titleNode + $valueTypeNode = $cfNode.SelectSingleNode("r:valueType", $ns) + $typeShort = Get-ValueTypeShorthand $valueTypeNode + $restrictTokens = Get-RestrictionTokens $cfNode.SelectSingleNode("r:useRestriction", $ns) + $appNode = $cfNode.SelectSingleNode("r:appearance", $ns) + $appearance = Get-AppearanceDict $appNode + + # multilingual title (non-ru) → object form + $titleNeedsObject = ($title -is [System.Collections.IDictionary]) -or ($typeShort -is [array]) + $needsObject = $appearance -or $titleNeedsObject + + if (-not $needsObject) { + # shorthand: "Name [Title]: type = expression #restrict" + $s = $dataPath + if ($title) { $s += " [$title]" } + if ($typeShort) { $s += ": $typeShort" } + if ($expression) { $s += " = $expression" } + if ($restrictTokens) { $s += ' ' + ($restrictTokens -join ' ') } + return $s + } + + $obj = [ordered]@{ name = $dataPath } + if ($title) { $obj['title'] = $title } + if ($typeShort) { $obj['type'] = $typeShort } + if ($expression) { $obj['expression'] = $expression } + if ($restrictTokens) { $obj['restrict'] = ($restrictTokens | ForEach-Object { $_ -replace '^#','' }) } + if ($appearance) { $obj['appearance'] = $appearance } + return $obj +} + +# Build totalField → shorthand or object form +function Build-TotalField { + param($tfNode) + $dataPath = Get-Text $tfNode "r:dataPath" + $expression = Get-Text $tfNode "r:expression" + # Detect Func() → shorthand "name: Func" + if ($expression -match '^(\w+)\(([^)]*)\)$') { + $func = $matches[1] + $inner = $matches[2].Trim() + if ($inner -eq $dataPath) { + return "$dataPath`: $func" + } + # "name: Func(expr)" form — also a valid shorthand + return "$dataPath`: $func($inner)" + } + # group attachment via groupItem — Ring 2 / object form + $groupNodes = $tfNode.SelectNodes("r:group", $ns) + $obj = [ordered]@{ dataPath = $dataPath; expression = $expression } + if ($groupNodes -and $groupNodes.Count -gt 0) { + $groups = @() + foreach ($g in $groupNodes) { $groups += $g.InnerText } + $obj['group'] = $groups + } + return $obj +} + +# Detect StandardPeriod variant from node +function Get-StandardPeriodVariant { + param($valueNode) + if (-not $valueNode) { return $null } + $variant = Get-Text $valueNode "v8:variant" + if ($variant) { return $variant } + return $null +} + +# Build parameter → shorthand or object form +function Build-Parameter { + param($pNode, [string]$loc) + $name = Get-Text $pNode "r:name" + $titleNode = $pNode.SelectSingleNode("r:title", $ns) + $title = Get-MLText $titleNode + $valueTypeNode = $pNode.SelectSingleNode("r:valueType", $ns) + $typeShort = Get-ValueTypeShorthand $valueTypeNode + + # value + $valueNode = $pNode.SelectSingleNode("r:value", $ns) + $valueDisplay = $null + $valueIsNil = $false + if ($valueNode) { + $nil = $valueNode.GetAttribute("nil", $NS_XSI) + if ($nil -eq 'true') { $valueIsNil = $true } + else { + $vType = Get-LocalXsiType $valueNode + if ($vType -eq 'StandardPeriod') { + $variant = Get-Text $valueNode "v8:variant" + if ($variant -and $variant -ne 'Custom') { $valueDisplay = $variant } + # Custom with explicit dates → object form (handled below via needsObject) + } elseif ($vType -eq 'DesignTimeValue') { + $valueDisplay = $valueNode.InnerText + } elseif ($vType -eq 'LocalStringType') { + $valueDisplay = Get-MLText $valueNode + } else { + $txt = $valueNode.InnerText + if ($txt) { $valueDisplay = $txt } + } + } + } + + $valueListAllowed = (Get-Text $pNode "r:valueListAllowed") -eq 'true' + $availableAsField = Get-Text $pNode "r:availableAsField" + $hidden = $availableAsField -eq 'false' + $denyIncomplete = (Get-Text $pNode "r:denyIncompleteValues") -eq 'true' + $useAttr = Get-Text $pNode "r:use" + $useRestriction = (Get-Text $pNode "r:useRestriction") -eq 'true' + $expression = Get-Text $pNode "r:expression" + + # availableValues + $avNodes = $pNode.SelectNodes("r:availableValue", $ns) + $availableValues = @() + foreach ($av in $avNodes) { + $avValNode = $av.SelectSingleNode("r:value", $ns) + $avPresNode = $av.SelectSingleNode("r:presentation", $ns) + $avEntry = [ordered]@{} + if ($avValNode) { $avEntry['value'] = $avValNode.InnerText } + if ($avPresNode) { $avEntry['presentation'] = Get-MLText $avPresNode } + $availableValues += $avEntry + } + + $flags = @() + + $result = [ordered]@{ + name = $name + title = $title + typeShort = $typeShort + valueDisplay = $valueDisplay + valueIsNil = $valueIsNil + valueListAllowed = $valueListAllowed + hidden = $hidden + denyIncomplete = $denyIncomplete + useAttr = $useAttr + useRestriction = $useRestriction + expression = $expression + availableValues = $availableValues + } + return $result +} + +# Render parameter (after autoDates folding) → shorthand or object form +function Render-Parameter { + param($p) + $name = $p.name + $title = $p.title + $typeShort = $p.typeShort + $valueDisplay = $p.valueDisplay + $valueIsNil = $p.valueIsNil + $flags = @() + if ($p.autoDates) { $flags += '@autoDates' } + if ($p.valueListAllowed) { $flags += '@valueList' } + if ($p.hidden) { $flags += '@hidden' } + + $titleNeedsObject = ($title -is [System.Collections.IDictionary]) + $typeIsArray = ($typeShort -is [array]) + $valueIsDict = ($valueDisplay -is [System.Collections.IDictionary]) + + # Object form needed if: availableValues, multilingual title, composite type, + # explicit denyIncomplete/use without @autoDates, useRestriction without autoDates, expression set + $needsObject = $false + if ($p.availableValues -and $p.availableValues.Count -gt 0) { $needsObject = $true } + if ($titleNeedsObject) { $needsObject = $true } + if ($typeIsArray) { $needsObject = $true } + if ($valueIsDict) { $needsObject = $true } + if (-not $p.autoDates) { + # @autoDates implies use=Always + denyIncomplete=true defaults — only object form if NOT autoDates + if ($p.denyIncomplete) { $needsObject = $true } + if ($p.useAttr) { $needsObject = $true } + } + # useRestriction is auto-generated by compile for @hidden params; ignore as object trigger + if ($p.expression) { $needsObject = $true } + + if (-not $needsObject) { + $s = $name + if ($title) { $s += " [$title]" } + if ($typeShort) { $s += ": $typeShort" } + if (-not $valueIsNil -and $null -ne $valueDisplay -and $valueDisplay -ne '') { $s += " = $valueDisplay" } + if ($flags) { $s += ' ' + ($flags -join ' ') } + return $s + } + + $obj = [ordered]@{ name = $name } + if ($title) { $obj['title'] = $title } + if ($typeShort) { $obj['type'] = $typeShort } + if (-not $valueIsNil -and $null -ne $valueDisplay -and $valueDisplay -ne '') { $obj['value'] = $valueDisplay } + if ($p.useAttr -and -not $p.autoDates) { $obj['use'] = $p.useAttr } + if ($p.denyIncomplete -and -not $p.autoDates) { $obj['denyIncompleteValues'] = $true } + if ($p.hidden) { $obj['hidden'] = $true } + if ($p.valueListAllowed) { $obj['valueListAllowed'] = $true } + if ($p.autoDates) { $obj['autoDates'] = $true } + if ($p.expression) { $obj['expression'] = $p.expression } + if ($p.availableValues -and $p.availableValues.Count -gt 0) { $obj['availableValues'] = $p.availableValues } + return $obj +} + # --- 4. dataSources --- $dataSources = @() @@ -357,11 +555,72 @@ foreach ($dsNode in $dsNodes) { $dataSets += $ds } +# --- 5b. calculatedFields --- + +$calculatedFields = @() +$cfNodes = $root.SelectNodes("r:calculatedField", $ns) +$ci = 0 +foreach ($cf in $cfNodes) { + $calculatedFields += (Build-CalcField -cfNode $cf -loc "calculatedField[$ci]") + $ci++ +} + +# --- 5c. totalFields --- + +$totalFields = @() +$tfNodes = $root.SelectNodes("r:totalField", $ns) +foreach ($tf in $tfNodes) { $totalFields += (Build-TotalField -tfNode $tf) } + +# --- 5d. parameters with autoDates folding --- + +$paramsRaw = @() +$pi = 0 +$pNodes = $root.SelectNodes("r:parameter", $ns) +foreach ($p in $pNodes) { + $paramsRaw += (Build-Parameter -pNode $p -loc "parameter[$pi]") + $pi++ +} + +# Detect autoDates: for each StandardPeriod parameter P, look for two siblings with +# expression "&P.ДатаНачала" and "&P.ДатаОкончания". If both found, mark P with @autoDates +# and remove the companions. +$paramByName = @{} +foreach ($p in $paramsRaw) { $paramByName[$p.name] = $p } + +$removedNames = @{} +foreach ($p in $paramsRaw) { + if ($p.typeShort -ne 'StandardPeriod') { continue } + $parentName = $p.name + $startExpr = '&' + $parentName + '.ДатаНачала' + $endExpr = '&' + $parentName + '.ДатаОкончания' + $startMatch = $null + $endMatch = $null + foreach ($q in $paramsRaw) { + if ($q.name -eq $parentName) { continue } + if ($q.expression -eq $startExpr) { $startMatch = $q.name } + elseif ($q.expression -eq $endExpr) { $endMatch = $q.name } + } + if ($startMatch -and $endMatch) { + $p['autoDates'] = $true + $removedNames[$startMatch] = $true + $removedNames[$endMatch] = $true + } +} + +$parameters = @() +foreach ($p in $paramsRaw) { + if ($removedNames.ContainsKey($p.name)) { continue } + $parameters += (Render-Parameter -p $p) +} + # --- 6. Build top-level JSON object --- $out = [ordered]@{} if ($emitDataSources) { $out['dataSources'] = $dataSources } $out['dataSets'] = $dataSets +if ($calculatedFields.Count -gt 0) { $out['calculatedFields'] = $calculatedFields } +if ($totalFields.Count -gt 0) { $out['totalFields'] = $totalFields } +if ($parameters.Count -gt 0) { $out['parameters'] = $parameters } # --- 7. Serialize --- @@ -394,7 +653,7 @@ if ($OutputPath) { Write-Host "Warnings: $wPath ($($script:warnings.Count) issue(s))" -ForegroundColor Yellow } - [Console]::Error.WriteLine("Decompiled: dataSets=$($dataSets.Count), warnings=$($script:warnings.Count)") + [Console]::Error.WriteLine("Decompiled: dataSets=$($dataSets.Count), calc=$($calculatedFields.Count), totals=$($totalFields.Count), params=$($parameters.Count), warnings=$($script:warnings.Count)") } else { Write-Output $json if ($script:warnings.Count -gt 0) { From 61c4bd418d55c9d12653db3221f90a8a3c307882 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 21 May 2026 14:58:48 +0300 Subject: [PATCH 005/138] =?UTF-8?q?feat(skd-decompile):=20=D1=81=D0=BB?= =?UTF-8?q?=D0=BE=D0=B8=206-9=20=E2=80=94=20templates=20(cells,=20merge,?= =?UTF-8?q?=20style,=20drilldown)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Парсинг