From be69bc231ca74bdd4f708060181d83431e6fe727 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 21 May 2026 14:41:15 +0300 Subject: [PATCH] =?UTF-8?q?feat(skd-decompile):=20=D1=81=D0=BB=D0=BE=D0=B8?= =?UTF-8?q?=204-5=20=E2=80=94=20calculatedFields,=20totalFields,=20paramet?= =?UTF-8?q?ers?= 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) {