From 7f3a8861ad007d037b2a090582d8d8c329ddba92 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 21 May 2026 19:53:41 +0300 Subject: [PATCH] =?UTF-8?q?feat(skd):=20inline=20cell=20style=20override?= =?UTF-8?q?=20+=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D0=B8=20C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cell в rows теперь может быть либо string ("text"/"{param}"/"|"/">"/null), либо объектом {value, style: "presetName"}. Object form применяется когда стиль ячейки отличается от template default. compile (ps1+py): helpers _get_cell_value / _get_cell_style_or_default. Emit-AreaTemplateDSL / _emit_area_template_dsl используют per-cell style для appearance вместо единого template style. decompile: refactor Build-Template. Первый pass — собрать style name per cell в cellStyleMap. Второй pass — выбрать template default как most frequent style. Третий pass — обернуть в {value, style} ячейки, чьи стили отличаются от default. TemplateStyleMismatch sentinel удалён — теперь все случаи покрываются через inline override. Дедуп при обоих pass'ах (Match-PresetByShape) работает через effectivePresets (built-in + user + ранее аллоцированные customN), так что одинаковые shape'ы получают одно имя. Новый тест template-inline-cell-style (round-trip bit-perfect). Versions: compile v1.32→v1.33, decompile v0.15→v0.16. Метрики на момент коммита: - ERP-сэмпл 30: 30/30 clean, 0 sentinel'ов - Корпус из 40 отчётов целевого класса: 40/40 clean, 0 sentinel'ов Закрывает категории A, B, C полностью на обоих корпусах. --- .../skd-compile/scripts/skd-compile.ps1 | 46 +++- .../skills/skd-compile/scripts/skd-compile.py | 40 ++- .../skd-decompile/scripts/skd-decompile.ps1 | 64 +++-- .../template-inline-cell-style/Template.xml | 228 ++++++++++++++++++ .../decompiled.json | 27 +++ .../template-inline-cell-style.json | 28 +++ 6 files changed, 392 insertions(+), 41 deletions(-) create mode 100644 tests/skills/cases/skd-decompile/snapshots/template-inline-cell-style/Template.xml create mode 100644 tests/skills/cases/skd-decompile/snapshots/template-inline-cell-style/decompiled.json create mode 100644 tests/skills/cases/skd-decompile/template-inline-cell-style.json diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 2668c83b..2104925a 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.32 — Compile 1C DCS from JSON +# skd-compile v1.33 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, @@ -1603,6 +1603,28 @@ function Emit-CellAppearance { X "`t`t`t`t" } +# Cell может быть string ("text"/"{param}"/"|"/">"/null) или объектом {value, style}. +# Helpers извлекают значение и эффективный стиль ячейки. +function Get-CellValue { + param($cell) + if ($null -eq $cell) { return $null } + if ($cell -is [string]) { return $cell } + if ($cell.PSObject -and $cell.PSObject.Properties['value']) { return $cell.value } + return $null +} + +function Get-CellStyleOrDefault { + param($cell, $defaultStyle) + if ($null -ne $cell -and -not ($cell -is [string]) -and $cell.PSObject -and $cell.PSObject.Properties['style']) { + $sName = "$($cell.style)" + if ($script:areaStylePresets.ContainsKey($sName)) { + return $script:areaStylePresets[$sName] + } + Write-Warning "Unknown cell style preset '$sName', falling back to template default" + } + return $defaultStyle +} + function Emit-AreaTemplateDSL { param($t) $styleName = if ($t.style) { "$($t.style)" } else { "data" } @@ -1622,10 +1644,8 @@ function Emit-AreaTemplateDSL { for ($r = $rows.Count - 1; $r -ge 1; $r--) { $vMerge[$r] = @{} for ($c = 0; $c -lt $colCount; $c++) { - $cellVal = $rows[$r][$c] - if ($cellVal -is [string] -and $cellVal -eq '|') { - $vMerge[$r][$c] = $true - } + $cellValStr = Get-CellValue $rows[$r][$c] + if ($cellValStr -eq '|') { $vMerge[$r][$c] = $true } } } if (-not $vMerge.ContainsKey(0)) { $vMerge[0] = @{} } @@ -1635,10 +1655,8 @@ function Emit-AreaTemplateDSL { for ($r = 0; $r -lt $rows.Count; $r++) { $hMerge[$r] = @{} for ($c = 0; $c -lt $colCount; $c++) { - $cellVal = $rows[$r][$c] - if ($cellVal -is [string] -and $cellVal -eq '>') { - $hMerge[$r][$c] = $true - } + $cellValStr = Get-CellValue $rows[$r][$c] + if ($cellValStr -eq '>') { $hMerge[$r][$c] = $true } } } @@ -1657,17 +1675,19 @@ function Emit-AreaTemplateDSL { for ($r = 0; $r -lt $rows.Count; $r++) { X "`t`t`t" for ($c = 0; $c -lt $colCount; $c++) { - $cellVal = $rows[$r][$c] + $cellRaw = $rows[$r][$c] + $cellVal = Get-CellValue $cellRaw + $cellStyle = Get-CellStyleOrDefault $cellRaw $style $w = if ($c -lt $widths.Count) { [double]$widths[$c] } else { 0 } $isVMerged = $vMerge[$r][$c] -eq $true $isHMerged = $hMerge[$r][$c] -eq $true X "`t`t`t`t" if ($isVMerged) { # Vertically merged cell — only appearance with vMerge flag + width - Emit-CellAppearance $style $w $true + Emit-CellAppearance $cellStyle $w $true } elseif ($isHMerged) { # Horizontally merged cell — only appearance with hMerge flag + width - Emit-CellAppearance $style $w $false $true + Emit-CellAppearance $cellStyle $w $false $true } else { # Cell value if ($null -ne $cellVal -and $cellVal -ne '') { @@ -1700,7 +1720,7 @@ function Emit-AreaTemplateDSL { # Appearance $h = if ($r -eq 0) { $minHeight } else { 0 } if (-not $cellExtraItems) { $cellExtraItems = @() } - Emit-CellAppearance $style $w $false $false $h $cellExtraItems + Emit-CellAppearance $cellStyle $w $false $false $h $cellExtraItems $cellExtraItems = @() } X "`t`t`t`t" diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py index c97fc6f1..3df68cd6 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.32 — Compile 1C DCS from JSON +# skd-compile v1.33 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -1325,6 +1325,26 @@ def _emit_cell_appearance(lines, style, width=0, v_merge=False, h_merge=False, m lines.append('\t\t\t\t') +# Cell может быть string ("text"/"{param}"/"|"/">"/null) или объектом {value, style}. +def _get_cell_value(cell): + if cell is None: + return None + if isinstance(cell, str): + return cell + if isinstance(cell, dict) and 'value' in cell: + return cell['value'] + return None + + +def _get_cell_style_or_default(cell, default_style): + if isinstance(cell, dict) and 'style' in cell: + s_name = str(cell['style']) + if s_name in AREA_STYLE_PRESETS: + return AREA_STYLE_PRESETS[s_name] + print(f"Warning: Unknown cell style preset '{s_name}', falling back to template default", file=sys.stderr) + return default_style + + def _emit_area_template_dsl(lines, t): style_name = str(t.get('style', '')) or 'data' if style_name not in AREA_STYLE_PRESETS: @@ -1342,8 +1362,8 @@ def _emit_area_template_dsl(lines, t): for r in range(len(rows) - 1, 0, -1): v_merge[r] = {} for c in range(col_count): - cell_val = rows[r][c] if c < len(rows[r]) else None - if isinstance(cell_val, str) and cell_val == '|': + cell_val = _get_cell_value(rows[r][c]) if c < len(rows[r]) else None + if cell_val == '|': v_merge[r][c] = True if 0 not in v_merge: v_merge[0] = {} @@ -1353,8 +1373,8 @@ def _emit_area_template_dsl(lines, t): for r in range(len(rows)): h_merge[r] = {} for c in range(col_count): - cell_val = rows[r][c] if c < len(rows[r]) else None - if isinstance(cell_val, str) and cell_val == '>': + cell_val = _get_cell_value(rows[r][c]) if c < len(rows[r]) else None + if cell_val == '>': h_merge[r][c] = True # Build drilldown map: param_name -> drilldown_value @@ -1371,15 +1391,17 @@ def _emit_area_template_dsl(lines, t): for r in range(len(rows)): lines.append('\t\t\t') for c in range(col_count): - cell_val = rows[r][c] if c < len(rows[r]) else None + cell_raw = rows[r][c] if c < len(rows[r]) else None + cell_val = _get_cell_value(cell_raw) + cell_style = _get_cell_style_or_default(cell_raw, style) w = float(widths[c]) if c < len(widths) else 0 is_v_merged = v_merge.get(r, {}).get(c, False) is_h_merged = h_merge.get(r, {}).get(c, False) lines.append('\t\t\t\t') if is_v_merged: - _emit_cell_appearance(lines, style, w, True) + _emit_cell_appearance(lines, cell_style, w, True) elif is_h_merged: - _emit_cell_appearance(lines, style, w, h_merge=True) + _emit_cell_appearance(lines, cell_style, w, h_merge=True) else: cell_extra_items = [] if cell_val is not None and str(cell_val) != '': @@ -1407,7 +1429,7 @@ def _emit_area_template_dsl(lines, t): emit_mltext(lines, '\t\t\t\t\t\t', 'dcsat:value', cell_str) lines.append('\t\t\t\t\t') h = min_height if r == 0 else 0 - _emit_cell_appearance(lines, style, w, False, False, h, cell_extra_items or None) + _emit_cell_appearance(lines, cell_style, w, False, False, h, cell_extra_items or None) lines.append('\t\t\t\t') lines.append('\t\t\t') diff --git a/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 b/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 index e639cd77..99356821 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.15 — Decompile 1C DCS Template.xml to JSON DSL (draft) +# skd-decompile v0.16 — Decompile 1C DCS Template.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -933,9 +933,8 @@ function Build-Template { $rows = @() $widths = $null $minHeight = $null - $detectedStyle = $null - $styleMismatch = $false - $hasAnyNonEmptyFp = $false # true если хоть одна ячейка имеет стилевые атрибуты + $cellStyleMap = @{} # "r,c" → имя стиля для конкретной ячейки (null для merge/no-style) + $hasAnyStyledCell = $false $drilldownByParam = @{} # param name → field name (X from Расшифровка_X) $rowIdx = 0 @@ -950,23 +949,17 @@ function Build-Template { $perCell = Get-CellPerCellAttrs $appNode $content = Get-CellContent $cellNode $perCell - # Style detection (skip empty cells with no appearance, and merge cells) + # Style detection (skip merge cells) if ($appNode -and -not $perCell.mergeV -and -not $perCell.mergeH) { $cellPreset = Extract-CellPreset $appNode if ($null -ne $cellPreset) { - # Ячейка имеет стилевые атрибуты — match против effectivePresets, иначе аллоцируем custom - $hasAnyNonEmptyFp = $true $matched = Match-PresetByShape $cellPreset if ($null -eq $matched) { $matched = Allocate-CustomStyle $cellPreset } - if ($null -eq $detectedStyle) { - $detectedStyle = $matched - } elseif ($matched -ne $detectedStyle) { - $styleMismatch = $true - } + $cellStyleMap["$rowIdx,$colIdx"] = $matched + $hasAnyStyledCell = $true } - # Если cellPreset = $null — ячейка без стилевых атрибутов (только per-cell width/merge), не контрибутирует. } # Drilldown attachment @@ -987,6 +980,42 @@ function Build-Template { $rowIdx++ } + # Template default = наиболее частый стиль ячеек. + $templateDefault = $null + if ($hasAnyStyledCell) { + $counts = @{} + foreach ($k in $cellStyleMap.Keys) { + $name = $cellStyleMap[$k] + if (-not $counts.ContainsKey($name)) { $counts[$name] = 0 } + $counts[$name]++ + } + $maxCount = 0 + foreach ($name in $counts.Keys) { + if ($counts[$name] -gt $maxCount) { + $maxCount = $counts[$name] + $templateDefault = $name + } + } + } + + # Если есть ячейки со стилем, отличным от template default — оборачиваем их в object form. + if ($templateDefault) { + $rowsOut = @() + for ($r = 0; $r -lt $rows.Count; $r++) { + $newRow = @() + for ($c = 0; $c -lt $rows[$r].Count; $c++) { + $key = "$r,$c" + if ($cellStyleMap.ContainsKey($key) -and $cellStyleMap[$key] -ne $templateDefault) { + $newRow += [ordered]@{ value = $rows[$r][$c]; style = $cellStyleMap[$key] } + } else { + $newRow += $rows[$r][$c] + } + } + $rowsOut += ,$newRow + } + $rows = $rowsOut + } + # Template parameters (and drilldown folding) $paramNodes = $templateNode.SelectNodes("r:parameter", $ns) $exprParams = [ordered]@{} @@ -1014,14 +1043,11 @@ function Build-Template { } # Decide output form - if ($detectedStyle -and -not $styleMismatch) { - $tmplObj['style'] = $detectedStyle - } elseif (-not $hasAnyNonEmptyFp -and $rows.Count -gt 0) { + if ($templateDefault) { + $tmplObj['style'] = $templateDefault + } elseif ($rows.Count -gt 0) { # Все ячейки без стилевых атрибутов — это шаблон "без стиля" $tmplObj['style'] = 'none' - } elseif ($styleMismatch -or ($null -eq $detectedStyle -and $hasAnyNonEmptyFp)) { - # Couldn't unify style — emit sentinel - $tmplObj['__unsupported__'] = (New-Sentinel -kind 'TemplateStyleMismatch' -loc $loc -detail 'Шаблон содержит ячейки с непокрытым/неоднородным оформлением (Кольцо 2)')['__unsupported__'] } if ($widths) { $tmplObj['widths'] = $widths } if ($minHeight) { $tmplObj['minHeight'] = $minHeight } diff --git a/tests/skills/cases/skd-decompile/snapshots/template-inline-cell-style/Template.xml b/tests/skills/cases/skd-decompile/snapshots/template-inline-cell-style/Template.xml new file mode 100644 index 00000000..1669953b --- /dev/null +++ b/tests/skills/cases/skd-decompile/snapshots/template-inline-cell-style/Template.xml @@ -0,0 +1,228 @@ + + + + ИсточникДанных1 + Local + + + Тест + + Поле + Поле + + xs:string + + 0 + Variable + + + + ИсточникДанных1 + ВЫБРАТЬ * ИЗ Справочник.Сотрудники + + + + Основной + + + ru + Основной + + + + + + + + + + + + + + + + diff --git a/tests/skills/cases/skd-decompile/snapshots/template-inline-cell-style/decompiled.json b/tests/skills/cases/skd-decompile/snapshots/template-inline-cell-style/decompiled.json new file mode 100644 index 00000000..ba6fd4f1 --- /dev/null +++ b/tests/skills/cases/skd-decompile/snapshots/template-inline-cell-style/decompiled.json @@ -0,0 +1,27 @@ +{ + "dataSets": [ + { + "name": "Тест", + "query": "ВЫБРАТЬ * ИЗ Справочник.Сотрудники", + "fields": [ + "Поле: string" + ] + } + ], + "templates": [ + { + "name": "СмешанныйМакет", + "style": "data", + "rows": [ + [ + "A", + { + "value": "B", + "style": "header" + }, + "C" + ] + ] + } + ] +} \ No newline at end of file diff --git a/tests/skills/cases/skd-decompile/template-inline-cell-style.json b/tests/skills/cases/skd-decompile/template-inline-cell-style.json new file mode 100644 index 00000000..7b32c298 --- /dev/null +++ b/tests/skills/cases/skd-decompile/template-inline-cell-style.json @@ -0,0 +1,28 @@ +{ + "name": "Шаблон с inline cell style (data + per-cell override)", + "preRun": [ + { + "script": "skd-compile/scripts/skd-compile", + "input": { + "dataSets": [{ + "name": "Тест", + "query": "ВЫБРАТЬ * ИЗ Справочник.Сотрудники", + "fields": ["Поле: string"] + }], + "templates": [ + { + "name": "СмешанныйМакет", + "style": "data", + "rows": [ + ["A", { "value": "B", "style": "header" }, "C"] + ] + } + ] + }, + "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "Template.xml" }, + "cwd": "{workDir}" + } + ], + "params": { "templatePath": "Template.xml" }, + "outputPath": "decompiled.json" +}