From abcd5be2b00c2f678185f221ed10f6e3297c2231 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Fri, 12 Jun 2026 20:55:00 +0300 Subject: [PATCH] =?UTF-8?q?feat(form-decompile,form-compile):=20presentati?= =?UTF-8?q?on=20=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=20CA/?= =?UTF-8?q?=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0=20=E2=80=94=20=D1=81?= =?UTF-8?q?=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D1=8B=20xs:string=20vs=20LocalStringType?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Топ-кластер нового baseline (~190 impact). элемента условного оформления и групп/сравнений фильтра: платформа хранит ru-only текст и как xs:string (плоский), и как LocalStringType (мультиязык-обёртка с одним ru). Декомпилятор схлопывал ru-only LocalStringType в строку (Get-MLText) → компилятор писал xs:string → mismatch. Плюс компилятор-баг: filter-item presentation эмитился через Emit-MLText (всегда мультиязык БЕЗ xsi:type), даже для плоской строки. Фикс: - Декомпилятор: Get-PresByType — ветвь по xsi:type, сохраняет {lang:text} объект для LocalStringType (даже один ru) vs плоскую строку для xs:string. Применён к presentation элемента CA (Build-ConditionalAppearance) и фильтра (group + comparison, Build-FilterItem). - Компилятор (ps1+py): filter-item presentation через by-form Emit-USPresentation/ emit_us_presentation (строка→xs:string, объект→LocalStringType с xsi:type). CA-item presentation компилятор уже эмитил by-form — не трогаем. Выборка 45 форм с LocalStringType-presentation: presentation-потерь 0, match 27→33, TOTAL 127→63, регрессий 0 (сверка с baseline). Кейс dynamic-list-form (+CA presentation {ru} ru-only + filter presentation объект/строка) сертифицирован загрузкой в 1С. Регресс 43/43, ps1==py (общий снэпшот на обоих рантаймах). baseline после кластера ListSettings/DataSet (A+B+C): match 1869→1975, TOTAL 3495→2557. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../form-compile/scripts/form-compile.ps1 | 6 +-- .../form-compile/scripts/form-compile.py | 6 +-- .../form-decompile/scripts/form-decompile.ps1 | 38 ++++++++++++++----- docs/form-dsl-spec.md | 4 +- .../cases/form-compile/dynamic-list-form.json | 6 +-- .../Товары/Forms/ФормаСписка/Ext/Form.xml | 13 +++++++ 6 files changed, 53 insertions(+), 20 deletions(-) diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 8c35aaff..d48fa5d9 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,4 +1,4 @@ -# form-compile v1.142 — Compile 1C managed form from JSON or object metadata +# form-compile v1.143 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, @@ -1696,7 +1696,7 @@ function Emit-FilterItem { Emit-FilterItem -item $sub -indent "$indent`t" } } - if ($item.presentation) { Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" } + if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" } if ($item.viewMode) { X "$indent`t$(Esc-Xml "$($item.viewMode)")" } if ($item.userSettingID) { $guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } @@ -1777,7 +1777,7 @@ function Emit-FilterItem { $vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" } X "$indent`t$vStr" } - if ($item.presentation) { Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" } + if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" } if ($item.viewMode) { X "$indent`t$(Esc-Xml "$($item.viewMode)")" } if ($item.userSettingID) { $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index ecf281fc..2c710409 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# form-compile v1.142 — Compile 1C managed form from JSON or object metadata +# form-compile v1.143 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy @@ -1449,7 +1449,7 @@ def emit_filter_item(lines, item, indent): sub = obj emit_filter_item(lines, sub, f'{indent}\t') if item.get('presentation'): - emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) + emit_us_presentation(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) if item.get('viewMode'): lines.append(f'{indent}\t{esc_xml(str(item["viewMode"]))}') if item.get('userSettingID'): @@ -1509,7 +1509,7 @@ def emit_filter_item(lines, item, indent): v_str = str(val).lower() if isinstance(val, bool) else esc_xml(str(val)) lines.append(f'{indent}\t{v_str}') if item.get('presentation'): - emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) + emit_us_presentation(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) if item.get('viewMode'): lines.append(f'{indent}\t{esc_xml(str(item["viewMode"]))}') if item.get('userSettingID'): diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index 28a96037..4fc4d2cd 100644 --- a/.claude/skills/form-decompile/scripts/form-decompile.ps1 +++ b/.claude/skills/form-decompile/scripts/form-decompile.ps1 @@ -1,4 +1,4 @@ -# form-decompile v0.115 — Decompile 1C managed Form.xml to JSON DSL (draft) +# form-decompile v0.116 — Decompile 1C managed Form.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. param( @@ -450,6 +450,26 @@ function Get-PresText { return $null } +# Presentation, сохраняющий ФОРМУ по xsi:type (не схлопывает ru-only LocalStringType в строку): +# <… xsi:type="v8:LocalStringType"> → объект {lang:text} (даже один ru), иначе xs:string → плоская строка. +# Нужно, чтобы компилятор воспроизвёл точный xsi:type (LocalStringType vs xs:string). Пустой LocalStringType → $null. +function Get-PresByType { + param($node) + if (-not $node) { return $null } + $xt = $node.GetAttribute("type", $NS_XSI) + if ($xt -match 'LocalStringType$') { + $d = [ordered]@{} + foreach ($it in $node.SelectNodes("v8:item", $ns)) { + $lang = Get-Text $it "v8:lang"; $content = Get-Text $it "v8:content" + if ($lang) { $d[$lang] = $content } + } + if ($d.Count -gt 0) { return $d } + return $null + } + if ($node.InnerText) { return $node.InnerText } + return $null +} + # Снять namespace-префикс с xsi:type ("dcsset:Foo" → "Foo") function Get-LocalXsiType { param($node) @@ -583,9 +603,9 @@ function Build-FilterItem { $gObj = [ordered]@{ group = $groupName; items = $items } $gPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns) if ($gPresNode) { - $gPres = Get-MLText $gPresNode - if (-not $gPres) { $gPres = $gPresNode.InnerText } - if ($gPres) { $gObj['presentation'] = $gPres } + # Сохраняем форму по xsi:type (LocalStringType ru-only ≠ xs:string) + $gPres = Get-PresByType $gPresNode + if ($null -ne $gPres -and $gPres -ne '') { $gObj['presentation'] = $gPres } } $gVMNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns) if ($gVMNode) { $gObj['viewMode'] = $gVMNode.InnerText } @@ -654,8 +674,8 @@ function Build-FilterItem { $fiPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns) $fiPres = $null if ($fiPresNode) { - $fiPres = Get-MLText $fiPresNode - if (-not $fiPres) { $fiPres = $fiPresNode.InnerText } + # Сохраняем форму по xsi:type (LocalStringType ru-only ≠ xs:string) + $fiPres = Get-PresByType $fiPresNode } $flags = @() @@ -874,9 +894,9 @@ function Build-ConditionalAppearance { if ($ap -and $ap.Count -gt 0) { $entry['appearance'] = $ap } $presNode = $it.SelectSingleNode("dcsset:presentation", $ns) if ($presNode) { - $pres = Get-MLText $presNode - if (-not $pres) { $pres = $presNode.InnerText } - if ($pres) { $entry['presentation'] = $pres } + # Сохраняем форму по xsi:type: LocalStringType (даже ru-only) → объект → компилятор эмитит LocalStringType. + $pres = Get-PresByType $presNode + if ($null -ne $pres -and $pres -ne '') { $entry['presentation'] = $pres } } $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) if ($vmN) { $entry['viewMode'] = $vmN.InnerText } diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md index ce48b68b..fee776c2 100644 --- a/docs/form-dsl-spec.md +++ b/docs/form-dsl-spec.md @@ -1052,7 +1052,7 @@ Shorthand: `"Имя [Заголовок]: тип = Выражение #noField # ``` - **order** — строка `"Поле"` (asc) / `"Поле desc"` (синонимы `убыв`/`desc`, `возр`/`asc`) / `"Auto"`, либо объект `{ field, direction?, use?, viewMode? }`. -- **filter** — shorthand `"Поле оператор значение @флаги"` (`@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`; `_` = пусто) или объект `{ field, op, value?, use?, userSettingID?, userSettingPresentation? }` или группа `{ group: "And"|"Or"|"Not", items: [...] }`. `userSettingPresentation` (кастомная подпись настройки) ведётся **по форме значения**: голая строка → `xsi:type="xs:string"`; объект `{ru,en}` → `v8:LocalStringType` (так же у `order`/`conditionalAppearance`/`dataParameters`). +- **filter** — shorthand `"Поле оператор значение @флаги"` (`@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`; `_` = пусто) или объект `{ field, op, value?, use?, userSettingID?, userSettingPresentation?, presentation? }` или группа `{ group: "And"|"Or"|"Not", items: [...] }`. `userSettingPresentation` (кастомная подпись настройки) и `presentation` (подпись условия/элемента) ведутся **по форме значения**: голая строка → `xsi:type="xs:string"`; объект `{ru,en}` (в т.ч. один `{ru}`) → `v8:LocalStringType` (так же у `order`/`conditionalAppearance`/`dataParameters` — и у `presentation` элемента условного оформления). - **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in`/`notIn`, `inHierarchy`/`inListByHierarchy`, `contains`/`notContains`, `beginsWith`/`notBeginsWith`, `like`/`notLike` (подобно; `%`-шаблон в значении, напр. `"КодВалют like %/ %"`), `filled`/`notFilled`. Регистр оператора не важен; у `like`/`notLike` есть рус. синоним `подобно`/`неподобно`. - **Дата в фильтре = `StandardBeginningDate`** (так платформа хранит дату-значение почти всегда — корпус 268 vs 2 `xs:dateTime`). Формы значения (от компактной к полной): - **голая ISO-дата** `"2020-01-01T00:00:00"` (без `valueType`) → `Custom` + эта дата. Работает и в shorthand: `"ДатаЗаказа > 2020-01-01T00:00:00"`. Это дефолт даты в фильтре. @@ -1167,7 +1167,7 @@ Shorthand: `"Имя [Заголовок]: тип = Выражение #noField # - `selection` — массив имён форматируемых полей (``). - `filter` — условие (filter-shorthand, как в СКД). - `appearance` — словарь «параметр-DCS: значение» (рус. verbatim: `ЦветТекста`/`ЦветФона`/`Шрифт`/…). Цвет → `v8ui:Color`. -- `presentation` — мультиязык → `xsi:type="v8:LocalStringType"`. +- `presentation` — подпись элемента оформления, **по форме значения**: голая строка → `xsi:type="xs:string"`; объект `{ru,en}` (в т.ч. один `{ru}`) → `v8:LocalStringType`. (Платформа хранит обе формы для одного ru-текста — различаем, чтобы не ломать раундтрип.) Декомпилятор/компилятор переиспользуют `Build-ConditionalAppearance`/`Emit-ConditionalAppearance` настроек списка (отличие — тег-обёртка `ConditionalAppearance` без `dcsset:` и без блок-мета). `scope` (привязка к области) в формах diff --git a/tests/skills/cases/form-compile/dynamic-list-form.json b/tests/skills/cases/form-compile/dynamic-list-form.json index cfa135d4..46503cfb 100644 --- a/tests/skills/cases/form-compile/dynamic-list-form.json +++ b/tests/skills/cases/form-compile/dynamic-list-form.json @@ -34,10 +34,10 @@ ], "order": [ "Description", "Code desc" ], "filter": [ "Артикул = _ @off @user", "Артикул like %тест%", "Description подобно %abc%", - { "field": "Code", "op": "=", "userSettingID": "auto", "userSettingPresentation": "Код товара" }, - { "field": "Description", "op": "=", "userSettingID": "auto", "userSettingPresentation": { "ru": "Наименование", "en": "Name" } } ], + { "field": "Code", "op": "=", "userSettingID": "auto", "userSettingPresentation": "Код товара", "presentation": { "ru": "Код равно" } }, + { "field": "Description", "op": "=", "userSettingID": "auto", "userSettingPresentation": { "ru": "Наименование", "en": "Name" }, "presentation": "Имя равно" } ], "dataParameters": [ "ВыборТовара @off" ], - "conditionalAppearance": [ { "filter": ["Артикул = _"], "appearance": { "ЦветТекста": "web:Red" } } ] + "conditionalAppearance": [ { "filter": ["Артикул = _"], "appearance": { "ЦветТекста": "web:Red" }, "presentation": { "ru": "Выделение пустых" } } ] } } ], "elements": [ diff --git a/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml index b9d743b6..8ef53a14 100644 --- a/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml +++ b/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml @@ -210,12 +210,19 @@ Code Equal + + + ru + Код равно + + UUID-003 Код товара Description Equal + Имя равно UUID-004 @@ -264,6 +271,12 @@ web:Red + + + ru + Выделение пустых + + Normal UUID-007