From 7892c9fbfa141a908c22e90764bb14585fed4c89 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 6 Aug 2026 18:00:31 +0300 Subject: [PATCH] =?UTF-8?q?fix(meta-compile):=20=D0=BF=D0=BE=D0=B7=D0=B8?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D1=83=D1=81=D0=BB=D0=BE=D0=B2=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D1=81=D1=82=D0=B0=D0=BD=D0=B4=D0=B0=D1=80=D1=82=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D1=80=D0=B5=D0=BA=D0=B2=D0=B8=D0=B7=D0=B8=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=B1=D1=83=D1=85=D1=80=D0=B5=D0=B3=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit У бухрегистра условные стандартные реквизиты стоят по обе стороны фикс-списка: PeriodAdjustment — перед Account, пары субконто — после Period. Эмиттер же клал все ключи, которых нет в списке типа, скопом вперёд, поэтому порядок расходился с выгрузкой. Разделено на три части: ключи вне списка по умолчанию идут ПЕРЕД ним (легаси вроде ExchangeDate у части планов обмена), подходящие под хвостовой шаблон типа — ПОСЛЕ, а именованные условные (PeriodAdjustment) занимают своё место в самом списке и эмитятся при наличии в DSL. Пары субконто заданы ШАБЛОНОМ, а не списком имён: их количество определяется свойством MaxExtDimensionCount плана счетов. В корпусе везде 3, но это однородность выборки, а не правило, и список из ExtDimension1..3 закрыл бы только наблюдаемый случай. Хвост сортируется по номеру, внутри номера ExtDimensionN идёт перед ExtDimensionTypeN. Проверка: - синтетика с ПЯТЬЮ парами субконто и перемешанными ключами на входе даёт канонический порядок с пропусками номеров; порты байт-в-байт (modulo UUID); - раундтрип «все виды» (112 объектов, 42 вида): порядок ≠ 0, TOTAL diff 0; - юнит-тесты 648/648 ps1, 645/648 py; 1С-сертификация accounting-register ✓. Co-Authored-By: Claude Opus 5 (1M context) --- .../meta-compile/scripts/meta-compile.ps1 | 45 ++++++++++++++++--- .../meta-compile/scripts/meta-compile.py | 45 ++++++++++++++++--- 2 files changed, 77 insertions(+), 13 deletions(-) diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index a5e18f94..32a48a71 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.86 — Compile 1C metadata object from JSON +# meta-compile v1.87 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -1300,7 +1300,7 @@ $script:standardAttributesByType = @{ "Enum" = @("Order","Ref") "InformationRegister" = @("Active","LineNumber","Recorder","Period") "AccumulationRegister" = @("Active","LineNumber","Recorder","Period") - "AccountingRegister" = @("Account","Active","LineNumber","Recorder","Period") + "AccountingRegister" = @("PeriodAdjustment","Account","Active","LineNumber","Recorder","Period") "CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry") "ChartOfAccounts" = @("PredefinedDataName","Order","OffBalance","Type","Description","Code","Parent","Predefined","DeletionMark","Ref") "ChartOfCharacteristicTypes" = @("PredefinedDataName","ValueType","Description","Code","IsFolder","Parent","Predefined","DeletionMark","Ref") @@ -1308,6 +1308,8 @@ $script:standardAttributesByType = @{ "BusinessProcess" = @("Started","HeadTask","Completed","Ref","DeletionMark","Date","Number") "Task" = @("Executed","Description","RoutePoint","BusinessProcess","Ref","DeletionMark","Date","Number") # Порядок в каждом списке — канон выгрузки, снят с корпуса acc+erp (внутри типа разброса нет). + # Условные члены перечислены в $script:stdAttrOptional — они эмитятся только при наличии + # ключа в DSL, но позицию берут отсюда. # У ПВХ IsFolder входит в фикс-список: он есть у всех 23 объектов корпуса с этим блоком. # У бухрегистра PeriodAdjustment, наоборот, условен (1 из 3) — он приходит как «лишний» # ключ и эмитится ПЕРЕД фикс-списком, что совпадает с платформой. @@ -1430,6 +1432,20 @@ function Emit-StandardAttribute { # - stdAttrProfile[тип]: профиль материализованного блока (пусто = schema-дефолт), поверх — DSL-override. # Миграция типа = добавить его в stdAttrConditionalTypes + stdAttrProfile и переснять снэпшоты; КОД НЕ ТРОГАЕМ. $script:stdAttrConditionalTypes = @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document') + +# Условные члены списка типа: позиция берётся из $script:standardAttributesByType, а сам +# реквизит эмитится только при наличии ключа в DSL (у бухрегистра PeriodAdjustment — 2 из 5). +$script:stdAttrOptional = @{ + "AccountingRegister" = @("PeriodAdjustment") +} + +# Хвостовая группа: реквизиты, которых нет в списке типа и которые идут ПОСЛЕ него. +# У бухрегистра это пары субконто. Именами их не перечислить: их количество задаётся +# свойством MaxExtDimensionCount плана счетов, а не константой (в корпусе везде 3, но +# это однородность выборки, а не правило). Поэтому — шаблон, а не список. +$script:stdAttrTailPattern = @{ + "AccountingRegister" = '^ExtDimension(Type)?\d+$' +} function Emit-StandardAttributes { param([string]$indent, [string]$objectType) $attrs = $script:standardAttributesByType[$objectType] @@ -1439,12 +1455,27 @@ function Emit-StandardAttributes { if ($conditional -and $null -eq $sa) { return } # условный тип без кастомизации → блока нет if ($sa -is [string] -and $sa -eq '') { return } # opt-out `standardAttributes:""` (дом-конвенция суппресса, ~5% регистров опускают all-default блок — правило не выводимо) $profile = $script:stdAttrProfile[$objectType]; if (-not $profile) { $profile = @{} } - # Доп. (опциональные) стандартные реквизиты вне фикс-списка типа — напр. ExchangeDate у части ПланОбмена - # (легаси, присутствие не выводится из свойств). Эмитим по факту наличия ключа в DSL, ПЕРЕД фикс-списком (их позиция). - $extra = @() - if ($sa) { foreach ($k in $sa.PSObject.Properties.Name) { if ($attrs -notcontains $k) { $extra += $k } } } + # Список типа задаёт ПОРЯДОК всех известных стандартных реквизитов, включая условные: + # их позиция бывает и до, и после обязательных (у бухрегистра PeriodAdjustment идёт + # перед Account, а ExtDimension1..3/ExtDimensionType1..3 — после Period), поэтому + # «условные скопом вперёд» не выражает канон. Условный эмитим по факту наличия в DSL. + $optional = $script:stdAttrOptional[$objectType]; if (-not $optional) { $optional = @() } + # Ключи, которых нет в списке типа ВООБЩЕ. По умолчанию их позиция — ПЕРЕД списком + # (легаси вроде ExchangeDate у части планов обмена). Подходящие под хвостовой шаблон + # типа идут ПОСЛЕ, в порядке номера, а внутри номера — сначала ExtDimensionN, затем + # ExtDimensionTypeN (порядок платформы). + $tailRe = $script:stdAttrTailPattern[$objectType] + $extra = @(); $tail = @() + if ($sa) { + foreach ($k in $sa.PSObject.Properties.Name) { + if ($attrs -contains $k) { continue } + if ($tailRe -and $k -match $tailRe) { $tail += $k } else { $extra += $k } + } + } + $tail = @($tail | Sort-Object @{e={[int]([regex]::Match($_, '\d+').Value)}}, @{e={ if ($_ -match 'Type\d+$') { 1 } else { 0 } }}) X "$indent" - foreach ($a in ($extra + $attrs)) { + foreach ($a in ($extra + $attrs + $tail)) { + if (($optional -contains $a) -and (-not ($sa -and $sa.PSObject.Properties[$a]))) { continue } $ov = @{} if ($profile.ContainsKey($a)) { foreach ($k in $profile[$a].Keys) { $ov[$k] = $profile[$a][$k] } } if ($sa) { # DSL-override применяем всегда при наличии ключа (для не-условных типов тоже, напр. ExchangePlan) diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py index e98ca59a..53f612b3 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.86 — Compile 1C metadata object from JSON +# meta-compile v1.87 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -1333,7 +1333,7 @@ standard_attributes_by_type = { 'Enum': ['Order', 'Ref'], 'InformationRegister': ['Active', 'LineNumber', 'Recorder', 'Period'], 'AccumulationRegister': ['Active', 'LineNumber', 'Recorder', 'Period'], - 'AccountingRegister': ['Account', 'Active', 'LineNumber', 'Recorder', 'Period'], + 'AccountingRegister': ['PeriodAdjustment', 'Account', 'Active', 'LineNumber', 'Recorder', 'Period'], 'CalculationRegister': ['Active', 'Recorder', 'LineNumber', 'RegistrationPeriod', 'CalculationType', 'ReversingEntry'], 'ChartOfAccounts': ['PredefinedDataName', 'Order', 'OffBalance', 'Type', 'Description', 'Code', 'Parent', 'Predefined', 'DeletionMark', 'Ref'], 'ChartOfCharacteristicTypes': ['PredefinedDataName', 'ValueType', 'Description', 'Code', 'IsFolder', 'Parent', 'Predefined', 'DeletionMark', 'Ref'], @@ -1467,6 +1467,20 @@ def emit_standard_attribute(indent, attr_name, ov=None): # std_attr_conditional_types: типы, где блок только при кастомизации (DSL-ключ standardAttributes). # Прочие типы → блок всегда (текущее поведение). Миграция типа = +строчка в оба справочника + снэпшоты. std_attr_conditional_types = {'Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document'} + +# Условные члены списка типа: позиция берётся из standard_attributes_by_type, а сам +# реквизит эмитится только при наличии ключа в DSL (у бухрегистра PeriodAdjustment — 2 из 5). +std_attr_optional = { + 'AccountingRegister': ('PeriodAdjustment',), +} + +# Хвостовая группа: реквизиты, которых нет в списке типа и которые идут ПОСЛЕ него. +# У бухрегистра это пары субконто. Именами их не перечислить: их количество задаётся +# свойством MaxExtDimensionCount плана счетов, а не константой (в корпусе везде 3, но +# это однородность выборки, а не правило). Поэтому — шаблон, а не список. +std_attr_tail_pattern = { + 'AccountingRegister': r'^ExtDimension(Type)?\d+$', +} def emit_standard_attributes(indent, object_type): attrs = standard_attributes_by_type.get(object_type) if not attrs: @@ -1478,11 +1492,30 @@ def emit_standard_attributes(indent, object_type): if isinstance(sa, str) and sa == '': return # opt-out `standardAttributes:""` (дом-конвенция суппресса, ~5% регистров опускают all-default блок) profile = std_attr_profile.get(object_type, {}) - # Доп. (опциональные) стандартные реквизиты вне фикс-списка — напр. ExchangeDate у части ПланОбмена - # (легаси, присутствие не выводится). Эмитим по факту ключа в DSL, ПЕРЕД фикс-списком (их позиция). - extra = [k for k in sa if k not in attrs] if isinstance(sa, dict) else [] + # Список типа задаёт ПОРЯДОК всех известных стандартных реквизитов, включая условные: + # их позиция бывает и до, и после обязательных (у бухрегистра PeriodAdjustment идёт + # перед Account, а ExtDimension1..3/ExtDimensionType1..3 — после Period), поэтому + # «условные скопом вперёд» не выражает канон. Условный эмитим по факту наличия в DSL. + optional = std_attr_optional.get(object_type, ()) + # Ключи, которых нет в списке типа ВООБЩЕ. По умолчанию их позиция — ПЕРЕД списком + # (легаси вроде ExchangeDate у части планов обмена). Подходящие под хвостовой шаблон + # типа идут ПОСЛЕ, в порядке номера, а внутри номера — сначала ExtDimensionN, затем + # ExtDimensionTypeN (порядок платформы). + tail_re = std_attr_tail_pattern.get(object_type) + extra, tail = [], [] + if isinstance(sa, dict): + for k in sa: + if k in attrs: + continue + if tail_re and re.match(tail_re, k): + tail.append(k) + else: + extra.append(k) + tail.sort(key=lambda k: (int(re.search(r'\d+', k).group()), 1 if re.search(r'Type\d+$', k) else 0)) X(f'{indent}') - for a in extra + list(attrs): + for a in extra + list(attrs) + tail: + if a in optional and not (isinstance(sa, dict) and a in sa): + continue ov = dict(profile.get(a, {})) if isinstance(sa, dict): d = sa.get(a)