feat(meta-compile,meta-decompile): свойства формата 2.20 (платформа 8.3.27)

Дельта формата 2.17→2.20 содержит три безусловных свойства, которых компилятор
не эмитил. Все три пишутся ТОЛЬКО при формате >= 2.20 (Detect-FormatVersion),
поэтому 2.17-проекты не меняются: полная сюита зелёная, ни один существующий
снэпшот не сдвинулся.

- xr:TypeReductionMode — каждому стандартному реквизиту, после CreateOnInput.
  TransformValues, кроме Owner → Deny (правило проверено против выгрузки acc:
  9 из 9 реквизитов совпали, включая Owner).
- TypeReductionMode — измерениям регистра СВЕДЕНИЙ (у прочих семейств и у
  реквизитов/ресурсов платформа его не пишет).
- LineNumberLength — табличным частям, последним в Properties.

LineNumberLength — прикладная возможность 8.3.27 (5..9 → до 999 999 999 строк
вместо 99 999), поэтому получил полноценный DSL-ключ и описание в spec §5.2.
Его дефолт зависит НЕ от версии формата, а от режима совместимости на момент
создания ТЧ (<=8_3_26 → 5, >=8_3_27 → 9) — платформа фиксирует значение и позже
не пересчитывает, поэтому в одной конфигурации соседствуют ТЧ с 5 и 9. Отсюда
новая Detect-CompatibilityMode: читает CompatibilityMode из Configuration.xml
(префикс 64 КБ — тег лежит на ~11-12 КБ, существующим 2000 байт не хватает).

Декомпилятор: TypeReductionMode захватывается только при отклонении от правила
(компилятор выводит его сам), LineNumberLength — всегда при наличии тега:
выводить его дефолт значило бы дублировать логику компилятора с риском разойтись.

Компараторы версий числовые по компонентам — строковое сравнение неверно
("2.9" > "2.17" лексикографически).

Тест-инфра: setup-фикстуры empty-config-220 и empty-config-220-compat24
(строятся тем же cf-init), два кейса — по одному на каждую ось.

Проверка: роундтрип реального 2.20-документа БП (АвансовыйОтчет, 7 ТЧ) —
по новым тегам 0 расхождений, значения и позиции совпали; остаточный хвост
52/39 идентичен такому же на 2.17, то есть пред-существующий. Сюита 570/570
ps1, 567+3 skipped py, ps1==py. 1С-сертификация обоих кейсов на 8.3.27 ✓.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-25 17:53:54 +03:00
co-authored by Claude Opus 5
parent 769b4d3dbd
commit 068928646d
23 changed files with 1912 additions and 19 deletions
@@ -1,4 +1,4 @@
# meta-compile v1.67 — Compile 1C metadata object from JSON
# meta-compile v1.68 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -1321,6 +1321,12 @@ function Emit-StandardAttribute {
X "$indent`t<xr:MultiLine>false</xr:MultiLine>"
X "$indent`t<xr:FillFromFillingValue>$ffv</xr:FillFromFillingValue>"
X "$indent`t<xr:CreateOnInput>Auto</xr:CreateOnInput>"
# Формат 2.20 (8.3.27): режим приведения типов. Платформа пишет его КАЖДОМУ стандартному
# реквизиту; значение всегда TransformValues, кроме владельца (Owner) — там Deny.
if ($script:isFormat220) {
$trm = OvOr 'TypeReductionMode' $(if ($attrName -ceq 'Owner') { 'Deny' } else { 'TransformValues' })
X "$indent`t<xr:TypeReductionMode>$trm</xr:TypeReductionMode>"
}
X "$indent`t<xr:MaxValue xsi:nil=`"true`"/>"
Emit-MLText "$indent`t" "xr:ToolTip" $tt
X "$indent`t<xr:ExtendedEdit>false</xr:ExtendedEdit>"
@@ -1949,6 +1955,12 @@ function Emit-Attribute {
X "$indent`t`t<DataHistory>$dh</DataHistory>"
}
}
# Формат 2.20 (8.3.27): режим приведения типов — последним в Properties и ТОЛЬКО у измерений
# регистра сведений (у реквизитов/ресурсов и у прочих семейств регистров платформа его не пишет).
if ($script:isFormat220 -and $elemTag -eq "Dimension" -and $context -eq "register-info") {
$trm = if ($parsed.typeReductionMode) { "$($parsed.typeReductionMode)" } else { "TransformValues" }
X "$indent`t`t<TypeReductionMode>$trm</TypeReductionMode>"
}
X "$indent`t</Properties>"
X "$indent</$elemTag>"
@@ -2020,7 +2032,7 @@ function Emit-Command {
# --- 9. TabularSection emitter ---
function Emit-TabularSection {
param([string]$indent, [string]$tsName, $columns, [string]$objectType, [string]$objectName, $tsSynonymArg = $null, $tsTooltip = $null, $tsComment = $null, $tsLineNumber = $null, $tsFillChecking = $null, $tsUse = $null)
param([string]$indent, [string]$tsName, $columns, [string]$objectType, [string]$objectName, $tsSynonymArg = $null, $tsTooltip = $null, $tsComment = $null, $tsLineNumber = $null, $tsFillChecking = $null, $tsUse = $null, $tsLineNumberLength = $null)
$uuid = New-Guid-String
X "$indent<TabularSection uuid=`"$uuid`">"
@@ -2059,6 +2071,12 @@ function Emit-TabularSection {
$use = if ($tsUse) { "$tsUse" } else { "ForItem" }
X "$indent`t`t<Use>$use</Use>"
}
# Формат 2.20 (8.3.27): длина номера строки ТЧ (5..9 → до 999 999 999 строк вместо 99 999).
# Последним в Properties. Дефолт платформа берёт из режима совместимости на момент создания ТЧ.
if ($script:isFormat220) {
$lnl = if ($null -ne $tsLineNumberLength -and "$tsLineNumberLength" -ne '') { [int]$tsLineNumberLength } else { $script:lineNumberLengthDefault }
X "$indent`t`t<LineNumberLength>$lnl</LineNumberLength>"
}
X "$indent`t</Properties>"
$tsContext = if ($objectType -in @("DataProcessor","Report")) { "processor-tabular" } else { "tabular" }
@@ -3984,7 +4002,51 @@ function Detect-FormatVersion([string]$dir) {
return "2.17"
}
# Режим совместимости конфигурации — из него выводится дефолт <LineNumberLength> табличной части
# (≤Version8_3_26 → 5, ≥Version8_3_27 → 9; платформа фиксирует значение при СОЗДАНии ТЧ).
# NB: версия ФОРМАТА от режима совместимости не зависит (её задаёт платформа выгрузки) — это
# независимые вещи, читаются из одного файла разными функциями.
# Читаем префикс побольше: <CompatibilityMode> лежит ~11-12 КБ от начала (в отличие от version=
# в первой строке), 2000 байт Detect-FormatVersion сюда не хватает.
function Detect-CompatibilityMode([string]$dir) {
$d = $dir
while ($d) {
$cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) {
# NB: длина файла — в БАЙТАХ, а Substring режет по СИМВОЛАМ (кириллица = 2 байта),
# поэтому ограничиваем по длине уже декодированной строки.
$text = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
$head = $text.Substring(0, [Math]::Min(65536, $text.Length))
if ($head -match '<CompatibilityMode>([^<]+)</CompatibilityMode>') { return $Matches[1].Trim() }
}
$parent = Split-Path $d -Parent
if ($parent -eq $d) { break }
$d = $parent
}
return "Version8_3_24"
}
# Номер версии режима совместимости для сравнений: "Version8_3_27" → 80327, "Version8_5_1" → 80501.
function Get-CompatModeRank([string]$mode) {
if ($mode -match '^Version(\d+)_(\d+)_(\d+)$') {
return [int]$Matches[1] * 10000 + [int]$Matches[2] * 100 + [int]$Matches[3]
}
return 0
}
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
$script:formatVersion = Detect-FormatVersion $OutputDir
$script:compatMode = Detect-CompatibilityMode $OutputDir
# Формат 2.20+ (платформа 8.3.27) — только тогда эмитим новые свойства.
$script:isFormat220 = (Get-FormatRank $script:formatVersion) -ge 220
# Дефолт длины номера строки ТЧ: с режима 8.3.27 платформа заводит новые ТЧ с 9 разрядами.
$script:lineNumberLengthDefault = if ((Get-CompatModeRank $script:compatMode) -ge 80327) { 9 } else { 5 }
# --- 15. Main assembler ---
@@ -4064,10 +4126,10 @@ if ($objType -in $typesWithAttrTS) {
# Нормализуем в $tsSections[name] = @{ columns; synonym; tooltip; comment }.
function New-TsEntry { param($val)
if ($val -is [array] -or $val.GetType().Name -eq 'Object[]') {
return @{ columns = @($val); synonym = $null; tooltip = $null; comment = $null; lineNumber = $null; fillChecking = $null; use = $null }
return @{ columns = @($val); synonym = $null; tooltip = $null; comment = $null; lineNumber = $null; fillChecking = $null; use = $null; lineNumberLength = $null }
}
$cols = if ($val.attributes) { @($val.attributes) } elseif ($val.columns) { @($val.columns) } else { @() }
return @{ columns = $cols; synonym = $val.synonym; tooltip = $val.tooltip; comment = if ($val.comment) { "$($val.comment)" } else { $null }; lineNumber = $val.lineNumber; fillChecking = $val.fillChecking; use = $val.use }
return @{ columns = $cols; synonym = $val.synonym; tooltip = $val.tooltip; comment = if ($val.comment) { "$($val.comment)" } else { $null }; lineNumber = $val.lineNumber; fillChecking = $val.fillChecking; use = $val.use; lineNumberLength = $val.lineNumberLength }
}
if ($def.tabularSections -is [array] -or $def.tabularSections.GetType().Name -eq "Object[]") {
foreach ($ts in $def.tabularSections) { $tsSections[$ts.name] = New-TsEntry $ts }
@@ -4117,7 +4179,7 @@ if ($objType -in $typesWithAttrTS) {
}
foreach ($tsName in $tsSections.Keys) {
$tsE = $tsSections[$tsName]
Emit-TabularSection "`t`t`t" $tsName $tsE.columns $objType $objName $tsE.synonym $tsE.tooltip $tsE.comment $tsE.lineNumber $tsE.fillChecking $tsE.use
Emit-TabularSection "`t`t`t" $tsName $tsE.columns $objType $objName $tsE.synonym $tsE.tooltip $tsE.comment $tsE.lineNumber $tsE.fillChecking $tsE.use $tsE.lineNumberLength
}
foreach ($af in $acctFlags) {
Emit-Attribute "`t`t`t" $af "account-flag" "AccountingFlag"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-compile v1.67 — Compile 1C metadata object from JSON
# meta-compile v1.68 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -1340,6 +1340,11 @@ def emit_standard_attribute(indent, attr_name, ov=None):
X(f'{indent}\t<xr:MultiLine>false</xr:MultiLine>')
X(f'{indent}\t<xr:FillFromFillingValue>{ffv}</xr:FillFromFillingValue>')
X(f'{indent}\t<xr:CreateOnInput>Auto</xr:CreateOnInput>')
# Формат 2.20 (8.3.27): режим приведения типов. Платформа пишет его КАЖДОМУ стандартному
# реквизиту; значение всегда TransformValues, кроме владельца (Owner) — там Deny.
if is_format_220:
trm = ov.get('TypeReductionMode', 'Deny' if attr_name == 'Owner' else 'TransformValues')
X(f'{indent}\t<xr:TypeReductionMode>{trm}</xr:TypeReductionMode>')
X(f'{indent}\t<xr:MaxValue xsi:nil="true"/>')
emit_mltext(f'{indent}\t', 'xr:ToolTip', tt)
X(f'{indent}\t<xr:ExtendedEdit>false</xr:ExtendedEdit>')
@@ -2012,6 +2017,10 @@ def emit_attribute(indent, parsed, context, elem_tag='Attribute'):
# DataHistory — not for Chart* types and non-InformationRegister register family
if context not in ('chart', 'register-other', 'register-accum', 'register-calc', 'register-account'):
X(f'{indent}\t\t<DataHistory>{parsed.get("dataHistory") or "Use"}</DataHistory>')
# Формат 2.20 (8.3.27): режим приведения типов — последним в Properties и ТОЛЬКО у измерений
# регистра сведений (у реквизитов/ресурсов и у прочих семейств регистров платформа его не пишет).
if is_format_220 and elem_tag == 'Dimension' and context == 'register-info':
X(f'{indent}\t\t<TypeReductionMode>{parsed.get("typeReductionMode") or "TransformValues"}</TypeReductionMode>')
X(f'{indent}\t</Properties>')
X(f'{indent}</{elem_tag}>')
@@ -2088,7 +2097,7 @@ def emit_command(indent, cmd_name, cmd):
X(f'{indent}\t</Properties>')
X(f'{indent}</Command>')
def emit_tabular_section(indent, ts_name, columns, object_type, object_name, ts_synonym_arg=None, ts_tooltip=None, ts_comment=None, ts_line_number=None, ts_fill_checking=None, ts_use=None):
def emit_tabular_section(indent, ts_name, columns, object_type, object_name, ts_synonym_arg=None, ts_tooltip=None, ts_comment=None, ts_line_number=None, ts_fill_checking=None, ts_use=None, ts_line_number_length=None):
uid = new_uuid()
X(f'{indent}<TabularSection uuid="{uid}">')
type_prefix = f'{object_type}TabularSection'
@@ -2119,6 +2128,11 @@ def emit_tabular_section(indent, ts_name, columns, object_type, object_name, ts_
emit_tabular_standard_attributes(f'{indent}\t\t', ts_line_number)
if object_type in ('Catalog', 'ChartOfCharacteristicTypes'):
X(f'{indent}\t\t<Use>{ts_use if ts_use else "ForItem"}</Use>')
# Формат 2.20 (8.3.27): длина номера строки ТЧ (5..9 → до 999 999 999 строк вместо 99 999).
# Последним в Properties. Дефолт платформа берёт из режима совместимости на момент создания ТЧ.
if is_format_220:
lnl = int(ts_line_number_length) if ts_line_number_length not in (None, '') else line_number_length_default
X(f'{indent}\t\t<LineNumberLength>{lnl}</LineNumberLength>')
X(f'{indent}\t</Properties>')
ts_context = 'processor-tabular' if object_type in ('DataProcessor', 'Report') else 'tabular'
X(f'{indent}\t<ChildObjects>')
@@ -3887,7 +3901,44 @@ def detect_format_version(d):
d = parent
return "2.17"
def detect_compatibility_mode(d):
"""Режим совместимости конфигурации — из него выводится дефолт <LineNumberLength> табличной части
(<=Version8_3_26 5, >=Version8_3_27 9; платформа фиксирует значение при СОЗДАНИИ ТЧ).
NB: версия ФОРМАТА от режима совместимости не зависит (её задаёт платформа выгрузки).
Читаем префикс побольше: <CompatibilityMode> лежит ~11-12 КБ от начала."""
while d:
cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f:
head = f.read(65536)
m = re.search(r'<CompatibilityMode>([^<]+)</CompatibilityMode>', head)
if m:
return m.group(1).strip()
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return "Version8_3_24"
def compat_mode_rank(mode):
""""Version8_3_27" → 80327, "Version8_5_1" → 80501."""
m = re.match(r'^Version(\d+)_(\d+)_(\d+)$', mode or '')
return int(m.group(1)) * 10000 + int(m.group(2)) * 100 + int(m.group(3)) if m else 0
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
format_version = detect_format_version(output_dir)
compat_mode = detect_compatibility_mode(output_dir)
# Формат 2.20+ (платформа 8.3.27) — только тогда эмитим новые свойства.
is_format_220 = format_rank(format_version) >= 220
# Дефолт длины номера строки ТЧ: с режима 8.3.27 платформа заводит новые ТЧ с 9 разрядами.
line_number_length_default = 9 if compat_mode_rank(compat_mode) >= 80327 else 5
# ---------------------------------------------------------------------------
# 15. Main assembler
@@ -3980,10 +4031,10 @@ if obj_type in types_with_attr_ts:
# Значение ТЧ: массив колонок (синоним авто) ЛИБО объект {attributes/columns, synonym, tooltip, comment}.
def new_ts_entry(val):
if isinstance(val, list):
return {'columns': val, 'synonym': None, 'tooltip': None, 'comment': None, 'lineNumber': None, 'fillChecking': None, 'use': None}
return {'columns': val, 'synonym': None, 'tooltip': None, 'comment': None, 'lineNumber': None, 'fillChecking': None, 'use': None, 'lineNumberLength': None}
cols = _as_list(val.get('attributes') or val.get('columns') or [])
return {'columns': cols, 'synonym': val.get('synonym'), 'tooltip': val.get('tooltip'),
'comment': str(val['comment']) if val.get('comment') else None, 'lineNumber': val.get('lineNumber'), 'fillChecking': val.get('fillChecking'), 'use': val.get('use')}
'comment': str(val['comment']) if val.get('comment') else None, 'lineNumber': val.get('lineNumber'), 'fillChecking': val.get('fillChecking'), 'use': val.get('use'), 'lineNumberLength': val.get('lineNumberLength')}
if isinstance(ts_data, list):
for ts in ts_data:
ts_sections[ts['name']] = new_ts_entry(ts)
@@ -4035,7 +4086,7 @@ if obj_type in types_with_attr_ts:
emit_attribute('\t\t\t', a, context)
for ts_name in ts_order:
e = ts_sections[ts_name]
emit_tabular_section('\t\t\t', ts_name, e['columns'], obj_type, obj_name, e['synonym'], e['tooltip'], e['comment'], e.get('lineNumber'), e.get('fillChecking'), e.get('use'))
emit_tabular_section('\t\t\t', ts_name, e['columns'], obj_type, obj_name, e['synonym'], e['tooltip'], e['comment'], e.get('lineNumber'), e.get('fillChecking'), e.get('use'), e.get('lineNumberLength'))
for af in acct_flags:
emit_attribute('\t\t\t', af, 'account-flag', 'AccountingFlag')
for edf in ext_dim_flags: