From 177c9bb0fa0fdb25296707c0e828f22f1dc67cc8 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 4 Jul 2026 18:03:18 +0300 Subject: [PATCH] =?UTF-8?q?feat(meta-compile,meta-decompile):=20extra-?= =?UTF-8?q?=D1=81=D0=B2=D0=BE=D0=B9=D1=81=D1=82=D0=B2=D0=B0=20=D1=80=D0=B5?= =?UTF-8?q?=D0=BA=D0=B2=D0=B8=D0=B7=D0=B8=D1=82=D0=B0=20minValue/maxValue/?= =?UTF-8?q?extendedEdit=20+=20String(N,fixed)=20(v1.31/v0.21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Компилятор хардкодил false, , и AllowedLength=Variable; декомпилятор эти свойства не захватывал. Реквизиты с диапазоном (год 2000-3999, код цены 1-3), фикс-длина строки, расширенное редактирование теряли данные при роундтрипе. Зеркало form-compile: • minValue/maxValue — граница диапазона, типизировано: JSON-число → xsi:type="xs:decimal", строка → xs:string (тип сохранён декомпилятором из XML). Хелпер Emit-MinMaxValue (применён к Emit-Attribute/Dimension/Resource). • extendedEdit — bool (многострочное поле). • String(N,fixed) → Fixed (фикс. длина); String(N)/String(N,variable) → Variable (дефолт). Парсер типа ps1+py. Ловушка: Parse-AttributeShorthand строит НОВЫЙ hashtable только с известными ключами — сначала забыл добавить minValue/maxValue/extendedEdit → $parsed.minValue был $null, компилятор молча писал nil, хотя декомпилятор значение отдавал. Роундтрип 120: match 100→108, TOTAL 120→78 (−42), Attribute>ExtendedEdit/ MaxValue/MinValue/AllowedLength=0, новых diff нет. Регресс 46/46 ps1+py, ps1↔py identical. spec §3.2/§4.2, кейс catalog-attr-props (+min/max/String(3,fixed)/extEdit). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../meta-compile/scripts/meta-compile.ps1 | 42 ++++-- .../meta-compile/scripts/meta-compile.py | 41 ++++-- .../meta-decompile/scripts/meta-decompile.ps1 | 24 +++- docs/meta-dsl-spec.md | 12 +- .../meta-compile/catalog-attr-props.json | 5 +- .../Catalogs/Контрагенты.xml | 133 ++++++++++++++++++ 6 files changed, 225 insertions(+), 32 deletions(-) diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index 2edbce71..b4b2b4af 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.ps1 +++ b/.claude/skills/meta-compile/scripts/meta-compile.ps1 @@ -1,4 +1,4 @@ -# meta-compile v1.30 — Compile 1C metadata object from JSON +# meta-compile v1.31 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -509,13 +509,14 @@ function Emit-TypeContent { return } - # String or String(N) - if ($typeStr -match '^String(\((\d+)\))?$') { + # String or String(N) or String(N,fixed|variable) — AllowedLength: Variable дефолт / Fixed (фикс. длина). + if ($typeStr -match '^String(\((\d+)(\s*,\s*(fixed|variable))?\))?$') { $len = if ($Matches[2]) { $Matches[2] } else { "10" } + $al = if ($Matches[4] -and $Matches[4].ToLower() -eq 'fixed') { 'Fixed' } else { 'Variable' } X "$indentxs:string" X "$indent" X "$indent`t$len" - X "$indent`tVariable" + X "$indent`t$al" X "$indent" return } @@ -829,6 +830,9 @@ function Parse-AttributeShorthand { format = $val.format editFormat = $val.editFormat mask = if ($val.mask) { "$($val.mask)" } else { "" } + extendedEdit = if ($val.extendedEdit -eq $true) { $true } else { $false } + minValue = $val.minValue + maxValue = $val.maxValue hasFillValue = ($val.PSObject -and $val.PSObject.Properties -and ($val.PSObject.Properties.Name -contains 'fillValue')) fillValue = $val.fillValue linkByType = $val.linkByType @@ -1481,6 +1485,15 @@ function Emit-Characteristics { X "$indent" } +# / — граница диапазона реквизита. Нет ключа → nil (не задано). Значение типизировано +# (зеркало form-compile): число → xs:decimal, строка → xs:string (тип сохранён декомпилятором). +function Emit-MinMaxValue { + param([string]$indent, [string]$tag, $val) + if ($null -eq $val) { X "$indent<$tag xsi:nil=`"true`"/>"; return } + $t = if ($val -is [string]) { 'xs:string' } else { 'xs:decimal' } + X "$indent<$tag xsi:type=`"$t`">$(Esc-Xml "$val")" +} + function Emit-Attribute { param([string]$indent, $parsed, [string]$context) # $context: "catalog", "document", "object", "processor", "tabular", "processor-tabular", "register" @@ -1525,9 +1538,10 @@ function Emit-Attribute { if ($parsed.mask) { X "$indent`t`t$(Esc-XmlText $parsed.mask)" } else { X "$indent`t`t" } $multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" } X "$indent`t`t$multiLine" - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`t" + $extEdit = if ($parsed.extendedEdit -eq $true) { "true" } else { "false" } + X "$indent`t`t$extEdit" + Emit-MinMaxValue "$indent`t`t" "MinValue" $parsed.minValue + Emit-MinMaxValue "$indent`t`t" "MaxValue" $parsed.maxValue # FillFromFillingValue — not for tabular/processor/chart/register-other # (Chart*, AccumulationRegister/AccountingRegister/CalculationRegister don't support these) @@ -1736,9 +1750,10 @@ function Emit-Dimension { X "$indent`t`t" $multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" } X "$indent`t`t$multiLine" - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`t" + $extEdit = if ($parsed.extendedEdit -eq $true) { "true" } else { "false" } + X "$indent`t`t$extEdit" + Emit-MinMaxValue "$indent`t`t" "MinValue" $parsed.minValue + Emit-MinMaxValue "$indent`t`t" "MaxValue" $parsed.maxValue # InformationRegister dimensions have FillFromFillingValue if ($registerType -eq "InformationRegister") { @@ -1830,9 +1845,10 @@ function Emit-Resource { X "$indent`t`t" $multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" } X "$indent`t`t$multiLine" - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`t" + $extEdit = if ($parsed.extendedEdit -eq $true) { "true" } else { "false" } + X "$indent`t`t$extEdit" + Emit-MinMaxValue "$indent`t`t" "MinValue" $parsed.minValue + Emit-MinMaxValue "$indent`t`t" "MaxValue" $parsed.maxValue # InformationRegister resources have FillFromFillingValue, FillValue if ($registerType -eq "InformationRegister") { diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py index 6132bfe4..723840b1 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.py +++ b/.claude/skills/meta-compile/scripts/meta-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# meta-compile v1.30 — Compile 1C metadata object from JSON +# meta-compile v1.31 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -550,14 +550,15 @@ def emit_type_content(indent, type_str): if type_str == 'Boolean': X(f'{indent}xs:boolean') return - # String or String(N) - m = re.match(r'^String(\((\d+)\))?$', type_str) + # String or String(N) or String(N,fixed|variable) — AllowedLength: Variable дефолт / Fixed (фикс. длина). + m = re.match(r'^String(\((\d+)(\s*,\s*(fixed|variable))?\))?$', type_str) if m: length = m.group(2) if m.group(2) else '10' + al = 'Fixed' if (m.group(4) and m.group(4).lower() == 'fixed') else 'Variable' X(f'{indent}xs:string') X(f'{indent}') X(f'{indent}\t{length}') - X(f'{indent}\tVariable') + X(f'{indent}\t{al}') X(f'{indent}') return # Number without params -> Number(10,0) @@ -853,6 +854,9 @@ def parse_attribute_shorthand(val): 'format': val.get('format'), 'editFormat': val.get('editFormat'), 'mask': str(val['mask']) if val.get('mask') else '', + 'extendedEdit': True if val.get('extendedEdit') is True else False, + 'minValue': val.get('minValue'), + 'maxValue': val.get('maxValue'), 'hasFillValue': ('fillValue' in val), 'fillValue': val.get('fillValue'), 'linkByType': val.get('linkByType'), @@ -1552,6 +1556,14 @@ def emit_characteristics(indent, chars): X(f'{indent}\t') X(f'{indent}') +def emit_min_max_value(indent, tag, val): + """/ — граница диапазона. None → nil. Число → xs:decimal, строка → xs:string.""" + if val is None: + X(f'{indent}<{tag} xsi:nil="true"/>') + return + t = 'xs:string' if isinstance(val, str) else 'xs:decimal' + X(f'{indent}<{tag} xsi:type="{t}">{esc_xml(str(val))}') + def emit_attribute(indent, parsed, context): attr_name = parsed['name'] ctx_reserved = RESERVED_BY_CONTEXT.get(context) @@ -1589,9 +1601,10 @@ def emit_attribute(indent, parsed, context): X(f'{indent}\t\t') multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false' X(f'{indent}\t\t{multi_line}') - X(f'{indent}\t\tfalse') - X(f'{indent}\t\t') - X(f'{indent}\t\t') + ext_edit = 'true' if parsed.get('extendedEdit') is True else 'false' + X(f'{indent}\t\t{ext_edit}') + emit_min_max_value(f'{indent}\t\t', 'MinValue', parsed.get('minValue')) + emit_min_max_value(f'{indent}\t\t', 'MaxValue', parsed.get('maxValue')) # FillFromFillingValue / FillValue — not for tabular/processor/chart/register-other # (Chart*, AccumulationRegister/AccountingRegister/CalculationRegister don't support these) if context not in ('tabular', 'processor', 'chart', 'register-other'): @@ -1776,9 +1789,10 @@ def emit_dimension(indent, parsed, register_type): X(f'{indent}\t\t') multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false' X(f'{indent}\t\t{multi_line}') - X(f'{indent}\t\tfalse') - X(f'{indent}\t\t') - X(f'{indent}\t\t') + ext_edit = 'true' if parsed.get('extendedEdit') is True else 'false' + X(f'{indent}\t\t{ext_edit}') + emit_min_max_value(f'{indent}\t\t', 'MinValue', parsed.get('minValue')) + emit_min_max_value(f'{indent}\t\t', 'MaxValue', parsed.get('maxValue')) flags = parsed.get('flags', []) if register_type == 'InformationRegister': fill_from = 'true' if 'master' in flags else 'false' @@ -1850,9 +1864,10 @@ def emit_resource(indent, parsed, register_type): X(f'{indent}\t\t') multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false' X(f'{indent}\t\t{multi_line}') - X(f'{indent}\t\tfalse') - X(f'{indent}\t\t') - X(f'{indent}\t\t') + ext_edit = 'true' if parsed.get('extendedEdit') is True else 'false' + X(f'{indent}\t\t{ext_edit}') + emit_min_max_value(f'{indent}\t\t', 'MinValue', parsed.get('minValue')) + emit_min_max_value(f'{indent}\t\t', 'MaxValue', parsed.get('maxValue')) if register_type == 'InformationRegister': X(f'{indent}\t\tfalse') X(f'{indent}\t\t') diff --git a/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 b/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 index 767be0e2..b621051c 100644 --- a/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 +++ b/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 @@ -1,4 +1,4 @@ -# meta-decompile v0.20 — XML объекта метаданных 1С → JSON-черновик формата meta-compile +# meta-decompile v0.21 — XML объекта метаданных 1С → JSON-черновик формата meta-compile # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # # Пилот: только Catalog. Инверс meta-compile (omit-on-default: ключ эмитим только @@ -156,9 +156,12 @@ function Get-TypeShorthand { switch -regex ($raw) { '(^|:)boolean$' { $parts += 'Boolean'; break } '(^|:)string$' { - $len = '10' - if ($next -and $next.LocalName -eq 'StringQualifiers') { $l = $next.SelectSingleNode('v8:Length', $nsm); if ($l) { $len = $l.InnerText } } - $parts += "String($len)"; break + $len = '10'; $al = '' + if ($next -and $next.LocalName -eq 'StringQualifiers') { + $l = $next.SelectSingleNode('v8:Length', $nsm); if ($l) { $len = $l.InnerText } + $aln = $next.SelectSingleNode('v8:AllowedLength', $nsm); if ($aln -and $aln.InnerText -eq 'Fixed') { $al = ',fixed' } + } + $parts += "String($len$al)"; break } '(^|:)decimal$' { $d = '10'; $f = '0'; $sign = '' @@ -279,6 +282,19 @@ function Attr-ToDsl { $v = & $en 'Mask'; if ($v) { $extra['mask'] = $v } $v = & $en 'ChoiceHistoryOnInput'; if ($v -and $v -ne 'Auto') { $extra['choiceHistoryOnInput'] = $v } $v = & $en 'FillChecking'; if ($v -eq 'ShowWarning') { $extra['fillChecking'] = 'ShowWarning' } + $v = & $en 'ExtendedEdit'; if ($v -eq 'true') { $extra['extendedEdit'] = $true } + # MinValue/MaxValue — граница диапазона (omit при nil). Тип сохраняем: xs:string→строка, xs:decimal→число. + foreach ($mm in @('MinValue','MaxValue')) { + $mn = $ap.SelectSingleNode("md:$mm", $nsm) + if (-not $mn) { continue } + if ($mn.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') -eq 'true') { continue } + $key = if ($mm -eq 'MinValue') { 'minValue' } else { 'maxValue' } + $xt = $mn.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance') + $txt = $mn.InnerText + if ($xt -match 'decimal|int|double|float') { + if ($txt -match '^-?\d+$') { $extra[$key] = [long]$txt } else { $extra[$key] = [double]$txt } + } else { $extra[$key] = $txt } + } $fmtV = Get-MLValue ($ap.SelectSingleNode('md:Format', $nsm)); if ($null -ne $fmtV) { $extra['format'] = $fmtV } $efmtV = Get-MLValue ($ap.SelectSingleNode('md:EditFormat', $nsm)); if ($null -ne $efmtV) { $extra['editFormat'] = $efmtV } diff --git a/docs/meta-dsl-spec.md b/docs/meta-dsl-spec.md index ace5e5af..d71952d5 100644 --- a/docs/meta-dsl-spec.md +++ b/docs/meta-dsl-spec.md @@ -59,7 +59,8 @@ JSON DSL для описания объектов метаданных конф | DSL | XML | |-----|-----| -| `String` или `String(100)` | `xs:string` + StringQualifiers | +| `String` или `String(100)` | `xs:string` + StringQualifiers (AllowedLength=Variable) | +| `String(100,fixed)` | `xs:string` + AllowedLength=Fixed (строка фикс. длины) | | `Number(15,2)` | `xs:decimal` + NumberQualifiers | | `Number(10,0,nonneg)` | `xs:decimal` + AllowedSign=Nonnegative | | `Boolean` | `xs:boolean` | @@ -180,6 +181,15 @@ JSON DSL для описания объектов метаданных конф | `choiceHistoryOnInput` | ChoiceHistoryOnInput | Auto | Auto/DontUse | | `indexing` | Indexing | DontIndex | флаги `index`/`indexAdditional` | | `multiLine` | MultiLine | false | bool (флаг `multiline`) | +| `extendedEdit` | ExtendedEdit | false | bool (расширенное редактирование — многострочное поле) | +| `minValue` / `maxValue` | MinValue / MaxValue | nil (не задано) | граница диапазона, типизировано (см. ниже) | + +**`minValue` / `maxValue`** — границы диапазона значений реквизита (``/``). Без ключа → +`xsi:nil="true"` (не задано). Значение **типизировано** (зеркало form-compile): JSON-**число** → `xsi:type="xs:decimal"`, +JSON-**строка** → `xsi:type="xs:string"` (напр. год `"2000"`, код `"01"`). Тип сохраняется декомпилятором из XML. + +**Фикс. длина строки:** тип `String(N,fixed)` → `Fixed` (строка ровно N символов); +`String(N)` / `String(N,variable)` → `Variable` (дефолт). См. §3.2. **`fillValue` — значение заполнения реквизита** (``). Пара с `fillFromFillingValue` — единый блок «заполнения» (недоступен у реквизитов ТЧ; там оба свойства не эмитятся). Форма **пустого** значения зависит от diff --git a/tests/skills/cases/meta-compile/catalog-attr-props.json b/tests/skills/cases/meta-compile/catalog-attr-props.json index ca9064f0..18a3230b 100644 --- a/tests/skills/cases/meta-compile/catalog-attr-props.json +++ b/tests/skills/cases/meta-compile/catalog-attr-props.json @@ -6,7 +6,10 @@ { "name": "ИНН", "type": "String(12)", "fullTextSearch": "DontUse", "comment": "Налоговый номер", "mask": "999999999999", "use": "ForFolderAndItem", "fillCheck": true }, { "name": "Курс", "type": "Number(15,4)", "format": "ЧДЦ=4", "editFormat": { "ru": "ЧДЦ=4" }, - "createOnInput": "Use", "quickChoice": false, "fillFromFillingValue": true, "dataHistory": "DontUse", "passwordMode": true } + "createOnInput": "Use", "quickChoice": false, "fillFromFillingValue": true, "dataHistory": "DontUse", "passwordMode": true }, + { "name": "ГодЗаключения", "type": "Number(4,0,nonneg)", "minValue": 2000, "maxValue": 3999 }, + { "name": "КодРегиона", "type": "String(3,fixed)", "minValue": "01", "maxValue": "99" }, + { "name": "Описание", "type": "String(500)", "extendedEdit": true } ] }, "validatePath": "Catalogs/Контрагенты", diff --git a/tests/skills/cases/meta-compile/snapshots/catalog-attr-props/Catalogs/Контрагенты.xml b/tests/skills/cases/meta-compile/snapshots/catalog-attr-props/Catalogs/Контрагенты.xml index 2c34738a..f857e51c 100644 --- a/tests/skills/cases/meta-compile/snapshots/catalog-attr-props/Catalogs/Контрагенты.xml +++ b/tests/skills/cases/meta-compile/snapshots/catalog-attr-props/Catalogs/Контрагенты.xml @@ -186,6 +186,139 @@ DontUse + + + ГодЗаключения + + + ru + Год заключения + + + + + xs:decimal + + 4 + 0 + Nonnegative + + + false + + + + false + + false + false + 2000 + 3999 + false + 0 + DontCheck + Items + + + Auto + Auto + + + Auto + ForItem + DontIndex + Use + Use + + + + + КодРегиона + + + ru + Код региона + + + + + xs:string + + 3 + Fixed + + + false + + + + false + + false + false + 01 + 99 + false + + DontCheck + Items + + + Auto + Auto + + + Auto + ForItem + DontIndex + Use + Use + + + + + Описание + + + ru + Описание + + + + + xs:string + + 500 + Variable + + + false + + + + false + + false + true + + + false + + DontCheck + Items + + + Auto + Auto + + + Auto + ForItem + DontIndex + Use + Use + +