feat(form-decompile,form-compile): динсписок DataSet/ListSettings — TypeId-verbatim + DataSetFieldNestedDataSet + ListSettings self-closing (механика кластера A)

Три механических фикса доминирующего кластера встроенных DCS-настроек динсписка
(152/690 дифф-форм rt-iter). Таргет-выборка 102 формы: TOTAL 657→280, match 0→82,
ноль регрессий.

1. TypeId-verbatim. Тип параметра/реквизита, заданный глобальным стабильным GUID
   (<v8:TypeId>, не <v8:Type>) — платформа так сериализует типы, чьё имя в контексте
   недоступно (определяемые/характеристики). Декомпилятор не ловил → параметр терял
   valueType. Маркер 'typeid:<GUID>' в грамматике типа: Decompile-Type ловит <v8:TypeId>,
   Emit-SingleType разворачивает обратно (как роль-по-GUID; GUID глобально стабилен →
   безопасно). Форма ОстаткиПартийСАТУРН/ФормаОстатков → match.

2. DataSetFieldNestedDataSet. Компилятор хардкодил xsi:type="DataSetFieldField" для
   всех полей набора → терял поле-вложенный набор (реквизит табличной части объекта).
   Маркер fields[].nested: декомпилятор ловит ...NestedDataSet, компилятор зеркалит.

3. ListSettings self-closing. Пустой дескриптор listSettings:{} эмитился парой
   <ListSettings></ListSettings>, оригинал — self-closing <ListSettings/> (70 форм
   корпуса). Зеркалим self-closing при пустом эмите (отслеживание буфера).

Остаток кластера (вне A, отдельные задачи): структура группировок списка
(dcsset:item StructureItemGroup), KeyField/CalculatedField/Field-valueType DataSet.

spec: fields.nested, listSettings:{} → self-closing, тип-токен typeid:<GUID>
(раундтрип, не для авторинга). Регресс form-compile 43/43 (ps1+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-06-12 17:16:43 +03:00
co-authored by Claude Opus 4.8
parent c383cc4ffe
commit 081d3a8a2f
4 changed files with 14925 additions and 14880 deletions
@@ -1,4 +1,4 @@
# form-compile v1.137 — Compile 1C managed form from JSON or object metadata # form-compile v1.138 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[string]$JsonPath, [string]$JsonPath,
@@ -2106,6 +2106,14 @@ function Emit-SingleType {
$typeStr = Resolve-TypeStr $typeStr $typeStr = Resolve-TypeStr $typeStr
# TypeId — тип, заданный глобальным стабильным GUID (<v8:TypeId>, не <v8:Type>). Платформа так
# сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID
# глобально стабилен → эмитим verbatim (как роль-по-GUID). Маркер декомпилятора: 'typeid:GUID'.
if ($typeStr -match '^typeid:([0-9a-fA-F-]{36})$') {
X "$indent<v8:TypeId>$($Matches[1])</v8:TypeId>"
return
}
# boolean # boolean
if ($typeStr -eq "boolean") { if ($typeStr -eq "boolean") {
X "$indent<v8:Type>xs:boolean</v8:Type>" X "$indent<v8:Type>xs:boolean</v8:Type>"
@@ -5471,7 +5479,10 @@ function Emit-Attributes {
# Явные поля набора (редко): override title/dataPath # Явные поля набора (редко): override title/dataPath
if ($st.fields) { if ($st.fields) {
foreach ($fld in $st.fields) { foreach ($fld in $st.fields) {
X "$si<Field xsi:type=`"dcssch:DataSetFieldField`">" # Тип поля набора: DataSetFieldField (дефолт) vs DataSetFieldNestedDataSet
# (поле-вложенный набор = реквизит табличной части; маркер nested).
$ftype = if ($fld.nested) { "DataSetFieldNestedDataSet" } else { "DataSetFieldField" }
X "$si<Field xsi:type=`"dcssch:$ftype`">"
$dp = if ($fld.dataPath) { $fld.dataPath } else { $fld.field } $dp = if ($fld.dataPath) { $fld.dataPath } else { $fld.field }
X "$si`t<dcssch:dataPath>$(Esc-Xml "$dp")</dcssch:dataPath>" X "$si`t<dcssch:dataPath>$(Esc-Xml "$dp")</dcssch:dataPath>"
X "$si`t<dcssch:field>$(Esc-Xml "$($fld.field)")</dcssch:field>" X "$si`t<dcssch:field>$(Esc-Xml "$($fld.field)")</dcssch:field>"
@@ -5491,7 +5502,9 @@ function Emit-Attributes {
# ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID. # ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID.
# Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы. # Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы.
$lsi = "$si`t" $lsi = "$si`t"
$lsOpenLen = $script:xml.Length
X "$si<ListSettings>" X "$si<ListSettings>"
$lsAfterOpenLen = $script:xml.Length # для self-closing, если внутри ничего не эмитнётся
if ($st.PSObject.Properties['listSettings'] -and $null -ne $st.listSettings) { if ($st.PSObject.Properties['listSettings'] -and $null -ne $st.listSettings) {
# Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой. # Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой.
# meta: 'v'=viewMode, 'u'=userSettingID (контейнеры); itemsViewMode/itemsUserSettingID → present. # meta: 'v'=viewMode, 'u'=userSettingID (контейнеры); itemsViewMode/itemsUserSettingID → present.
@@ -5516,7 +5529,13 @@ function Emit-Attributes {
X "$lsi<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>" X "$lsi<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>"
X "$lsi<dcsset:itemsUserSettingID>$($script:CANON_ITEMS_ID)</dcsset:itemsUserSettingID>" X "$lsi<dcsset:itemsUserSettingID>$($script:CANON_ITEMS_ID)</dcsset:itemsUserSettingID>"
} }
if ($script:xml.Length -eq $lsAfterOpenLen) {
# Пустой дескриптор listSettings:{} (оригинал = <ListSettings/>) → зеркалим self-closing.
$script:xml.Length = $lsOpenLen
X "$si<ListSettings/>"
} else {
X "$si</ListSettings>" X "$si</ListSettings>"
}
X "$inner</Settings>" X "$inner</Settings>"
} }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-compile v1.137 — Compile 1C managed form from JSON or object metadata # form-compile v1.138 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import copy import copy
@@ -3373,6 +3373,13 @@ def resolve_type_str(type_str):
def emit_single_type(lines, type_str, indent): def emit_single_type(lines, type_str, indent):
type_str = resolve_type_str(type_str) type_str = resolve_type_str(type_str)
# TypeId — тип, заданный глобальным стабильным GUID (<v8:TypeId>, не <v8:Type>). Платформа так
# сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID
# глобально стабилен → эмитим verbatim (как роль-по-GUID). Маркер декомпилятора: 'typeid:GUID'.
m = re.match(r'^typeid:([0-9a-fA-F-]{36})$', type_str)
if m:
lines.append(f'{indent}<v8:TypeId>{m.group(1)}</v8:TypeId>')
return
# boolean # boolean
if type_str == 'boolean': if type_str == 'boolean':
lines.append(f'{indent}<v8:Type>xs:boolean</v8:Type>') lines.append(f'{indent}<v8:Type>xs:boolean</v8:Type>')
@@ -5212,7 +5219,10 @@ def emit_attributes(lines, attrs, indent, conditional_appearance=None):
# Явные поля набора (редко): override title/dataPath # Явные поля набора (редко): override title/dataPath
if s.get('fields'): if s.get('fields'):
for fld in s['fields']: for fld in s['fields']:
lines.append(f'{si}<Field xsi:type="dcssch:DataSetFieldField">') # Тип поля набора: DataSetFieldField (дефолт) vs DataSetFieldNestedDataSet
# (поле-вложенный набор = реквизит табличной части; маркер nested).
ftype = 'DataSetFieldNestedDataSet' if fld.get('nested') else 'DataSetFieldField'
lines.append(f'{si}<Field xsi:type="dcssch:{ftype}">')
dp = fld.get('dataPath') or fld.get('field') dp = fld.get('dataPath') or fld.get('field')
lines.append(f'{si}\t<dcssch:dataPath>{esc_xml(str(dp))}</dcssch:dataPath>') lines.append(f'{si}\t<dcssch:dataPath>{esc_xml(str(dp))}</dcssch:dataPath>')
lines.append(f'{si}\t<dcssch:field>{esc_xml(str(fld.get("field", "")))}</dcssch:field>') lines.append(f'{si}\t<dcssch:field>{esc_xml(str(fld.get("field", "")))}</dcssch:field>')
@@ -5232,6 +5242,7 @@ def emit_attributes(lines, attrs, indent, conditional_appearance=None):
# Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы. # Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы.
lsi = f'{si}\t' lsi = f'{si}\t'
lines.append(f'{si}<ListSettings>') lines.append(f'{si}<ListSettings>')
ls_open_idx = len(lines) - 1 # для self-closing, если внутри ничего не эмитнётся
ls_shape = s.get('listSettings') ls_shape = s.get('listSettings')
if ls_shape is not None: if ls_shape is not None:
# Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой. # Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой.
@@ -5261,6 +5272,10 @@ def emit_attributes(lines, attrs, indent, conditional_appearance=None):
emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_CA_ID) emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_CA_ID)
lines.append(f'{lsi}<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>') lines.append(f'{lsi}<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>')
lines.append(f'{lsi}<dcsset:itemsUserSettingID>{CANON_ITEMS_ID}</dcsset:itemsUserSettingID>') lines.append(f'{lsi}<dcsset:itemsUserSettingID>{CANON_ITEMS_ID}</dcsset:itemsUserSettingID>')
if len(lines) - 1 == ls_open_idx:
# Пустой дескриптор listSettings:{} (оригинал = <ListSettings/>) → зеркалим self-closing.
lines[ls_open_idx] = f'{si}<ListSettings/>'
else:
lines.append(f'{si}</ListSettings>') lines.append(f'{si}</ListSettings>')
lines.append(f'{inner}</Settings>') lines.append(f'{inner}</Settings>')
@@ -1,4 +1,4 @@
# form-decompile v0.110 — Decompile 1C managed Form.xml to JSON DSL (draft) # form-decompile v0.111 — Decompile 1C managed Form.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
param( param(
@@ -1180,6 +1180,12 @@ function Decompile-Type {
$short = $raw -replace '^(v8ui|v8|cfg):', '' $short = $raw -replace '^(v8ui|v8|cfg):', ''
[void]$parts.Add($short) [void]$parts.Add($short)
} }
# TypeId — тип, заданный глобальным стабильным GUID (<v8:TypeId>, не <v8:Type>). Платформа так
# сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID глобально
# стабилен → эмитим verbatim как маркер 'typeid:GUID' (компилятор разворачивает обратно; как роль-по-GUID).
foreach ($ti in @($typeNode.SelectNodes("v8:TypeId", $ns))) {
[void]$parts.Add("typeid:" + $ti.InnerText.Trim())
}
if ($parts.Count -eq 0) { return $null } if ($parts.Count -eq 0) { return $null }
if ($parts.Count -eq 1) { return $parts[0] } if ($parts.Count -eq 1) { return $parts[0] }
return ($parts -join ' | ') return ($parts -join ' | ')
@@ -2507,6 +2513,9 @@ if ($attrsNode) {
$dp = Get-Child $fn 'dataPath' $dp = Get-Child $fn 'dataPath'
if ($fld) { $fo['field'] = $fld } if ($fld) { $fo['field'] = $fld }
if ($dp -and $dp -ne $fld) { $fo['dataPath'] = $dp } if ($dp -and $dp -ne $fld) { $fo['dataPath'] = $dp }
# Тип поля набора: DataSetFieldField (дефолт, компилятор хардкодит) vs
# DataSetFieldNestedDataSet (поле-вложенный набор = реквизит табличной части). Маркер nested.
if ($fn.GetAttribute("type", $NS_XSI) -match 'NestedDataSet$') { $fo['nested'] = $true }
$ftn = $fn.SelectSingleNode("dcssch:title", $ns) $ftn = $fn.SelectSingleNode("dcssch:title", $ns)
if ($ftn) { $t = Get-LangText $ftn; if ($null -ne $t) { $fo['title'] = $t } } if ($ftn) { $t = Get-LangText $ftn; if ($null -ne $t) { $fo['title'] = $t } }
[void]$fields.Add($fo) [void]$fields.Add($fo)
+4 -2
View File
@@ -940,7 +940,7 @@ Forgiving-синонимы типа: XML-имя (`SpreadSheetDocumentField`) и
| `query` | string | Текст запроса (`ManualQuery=true`). Поддерживает `@file.sql` (путь относительно JSON) | | `query` | string | Текст запроса (`ManualQuery=true`). Поддерживает `@file.sql` (путь относительно JSON) |
| `dynamicDataRead` | bool | Динамическое считывание. **Умолчание `true`** — указывать только для отключения (`false`) | | `dynamicDataRead` | bool | Динамическое считывание. **Умолчание `true`** — указывать только для отключения (`false`) |
| `autoFillAvailableFields` | bool | Автозаполнение доступных полей (`<AutoFillAvailableFields>`). **Умолчание `true`** — указывать только для отключения (`false`; тогда поля берутся из явного запроса, не авто). Эмитится первым в `<Settings>` | | `autoFillAvailableFields` | bool | Автозаполнение доступных полей (`<AutoFillAvailableFields>`). **Умолчание `true`** — указывать только для отключения (`false`; тогда поля берутся из явного запроса, не авто). Эмитится первым в `<Settings>` |
| `fields` | array | Явные поля набора (редко): `{ field, dataPath?, title? }` — для переопределения заголовка. Обычно поля выводятся из запроса автоматически | | `fields` | array | Явные поля набора (редко): `{ field, dataPath?, title?, nested? }` — для переопределения заголовка. `nested: true` помечает поле-вложенный набор (`DataSetFieldNestedDataSet` = реквизит табличной части объекта; дефолт — `DataSetFieldField`). Обычно поля выводятся из запроса автоматически |
| `parameters` | array | Параметры схемы запроса (`DataCompositionSchemaParameter`) — см. ниже | | `parameters` | array | Параметры схемы запроса (`DataCompositionSchemaParameter`) — см. ниже |
| `order` | array | Сортировка списка (см. ниже) | | `order` | array | Сортировка списка (см. ниже) |
| `filter` | array | Отбор списка (грамматика как в СКД) | | `filter` | array | Отбор списка (грамматика как в СКД) |
@@ -951,7 +951,7 @@ Forgiving-синонимы типа: XML-имя (`SpreadSheetDocumentField`) и
Пустой блок настроек компоновщика (`ListSettings`) генерируется автоматически (каноничный полный скелет платформы — filter+order+conditionalAppearance+itemsViewMode+itemsUserSettingID, ~93% форм); указывать ничего не нужно. Пустой блок настроек компоновщика (`ListSettings`) генерируется автоматически (каноничный полный скелет платформы — filter+order+conditionalAppearance+itemsViewMode+itemsUserSettingID, ~93% форм); указывать ничего не нужно.
| `listSettings` | object | **Дескриптор формы скелета `<ListSettings>`** — только для НЕ-каноничных (частичных/минимальных) форм. Ordered-карта present top-level элементов: контейнеры `filter`/`order`/`conditionalAppearance` → блок-мета (`"vu"`=viewMode+userSettingID, `"u"`=только userSettingID, `"v"`, `""`); `itemsViewMode`/`itemsUserSettingID``true`. Компилятор эмитит ТОЛЬКО указанные части (контент берёт из `filter`/`order`/`conditionalAppearance`). Нет ключа → полный каноничный скелет. Декомпилятор пишет дескриптор только для отклонений от канона | | `listSettings` | object | **Дескриптор формы скелета `<ListSettings>`** — только для НЕ-каноничных (частичных/минимальных) форм. Ordered-карта present top-level элементов: контейнеры `filter`/`order`/`conditionalAppearance` → блок-мета (`"vu"`=viewMode+userSettingID, `"u"`=только userSettingID, `"v"`, `""`); `itemsViewMode`/`itemsUserSettingID``true`. Компилятор эмитит ТОЛЬКО указанные части (контент берёт из `filter`/`order`/`conditionalAppearance`). Нет ключа → полный каноничный скелет. Пустой объект `{}` → self-closing `<ListSettings/>` (оригинал без скелета). Декомпилятор пишет дескриптор только для отклонений от канона |
#### parameters — параметры схемы дин-списка #### parameters — параметры схемы дин-списка
@@ -979,6 +979,8 @@ Forgiving-синонимы типа: XML-имя (`SpreadSheetDocumentField`) и
Объектные ключи (как в СКД): `name`, `title`, `type`/`valueType`, `value`, `valueListAllowed`, `useRestriction`, `availableAsField`, `expression`, `availableValues` (`[{ value, presentation }]`), `inputParameters`, `denyIncompleteValues`, `use`. Объектные ключи (как в СКД): `name`, `title`, `type`/`valueType`, `value`, `valueListAllowed`, `useRestriction`, `availableAsField`, `expression`, `availableValues` (`[{ value, presentation }]`), `inputParameters`, `denyIncompleteValues`, `use`.
> **Тип-токен `typeid:<GUID>`** (раундтрип, не для ручного авторинга) — тип, заданный глобальным стабильным GUID (`<v8:TypeId>`, не `<v8:Type>`). Платформа так сериализует типы, чьё имя в данном контексте недоступно (определяемые типы / характеристики). GUID глобально стабилен → эмитится verbatim. Применим везде, где принимается `type`/`valueType` (параметры, реквизиты). Декомпилятор ставит его сам; вручную указывают только реальный существующий GUID типа конфигурации.
> **`value: null` при `valueListAllowed: true`** — явный маркер «эмитить `<dcssch:value xsi:nil/>`». Платформа пишет nil-значение для valueListAllowed-параметра не всегда (корпус 27 с / 47 без); по умолчанию (ключ `value` отсутствует) компилятор его НЕ эмитит. Декомпилятор ставит `value: null`, когда оригинал содержит nil-тег. > **`value: null` при `valueListAllowed: true`** — явный маркер «эмитить `<dcssch:value xsi:nil/>`». Платформа пишет nil-значение для valueListAllowed-параметра не всегда (корпус 27 с / 47 без); по умолчанию (ключ `value` отсутствует) компилятор его НЕ эмитит. Декомпилятор ставит `value: null`, когда оригинал содержит nil-тег.
#### order / filter / conditionalAppearance #### order / filter / conditionalAppearance