fix(meta-compile): нормализация MDObjectRef — CatalogRef./русская запись → Catalog.

MDObjectRef ссылается на ОБЪЕКТ метаданных (Catalog.Валюты), а не на тип ссылки.
Эмиттеры пропускали значение как есть, если в нём была точка, поэтому
"CatalogRef.Валюты" доходил до XML без изменений → при загрузке конфигурации
платформа отвечала «Неизвестный объект метаданных».

Инструкция вела в баг сама: reference/catalog.md документировал
owners: ["CatalogRef.Контрагенты"]. Тестами не ловилось — все кейсы
использовали каноническую форму.

- Normalize-MDObjectRef расширена ссылочными формами (англ. *Ref + рус. *Ссылка);
  вида метаданных, оканчивающегося на Ref, не существует → схлопывание однозначно.
  В ТИПАХ реквизитов запись CatalogRef.X верна — там мапа не применяется.
- Добавлен параметр defaultRoot (голое имя без точки), инлайн-подстановка
  "Catalog.$ownerRef" в owners убрана — логика теперь в одном месте.
- Нормализация применена в 4 местах, где её не было: owners, basedOn,
  registerRecords, baseCalculationTypes.
- Кейс catalog-inputbystring-datalock переведён на неканонический вход:
  снэпшот не изменился ни на байт — прямое доказательство нормализации.

Регресс 73/73 ps1+py, полная сюита 566/566. Живая проверка на 8.3.27:
подчинённый справочник с owners CatalogRef./СправочникСсылка. грузится чисто.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-25 14:26:55 +03:00
co-authored by Claude Opus 5
parent e01688e764
commit 66d45d3654
4 changed files with 51 additions and 21 deletions
@@ -15,7 +15,7 @@
| `limitLevelCount` | `false` | bool (ограничивать кол-во уровней) |
| `levelCount` | `2` | число уровней (при `limitLevelCount`) |
| `foldersOnTop` | `true` | bool (группы сверху) |
| `owners` | `[]` | массив ссылок-владельцев: `["CatalogRef.Контрагенты"]` |
| `owners` | `[]` | массив владельцев: `["Catalog.Контрагенты"]` |
| `subordinationUse` | `ToItems` | `ToItems` / `ToFolders` / `ToFoldersAndItems` (кому подчинён) |
| `codeLength` | `9` | длина кода (0 — без кода) |
| `codeType` | `String` | `String` / `Number` |
@@ -1,4 +1,4 @@
# meta-compile v1.66 — Compile 1C metadata object from JSON
# meta-compile v1.67 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -786,11 +786,28 @@ $script:mdRefRoots = @{
'журналдокументов'='DocumentJournal'; 'отчет'='Report'; 'отчёт'='Report'; 'обработка'='DataProcessor';
'табличнаячасть'='TabularSection'; 'реквизит'='Attribute'; 'измерение'='Dimension'; 'ресурс'='Resource';
'стандартныйреквизит'='StandardAttribute'; 'значениеперечисления'='EnumValue'; 'команда'='Command';
'признакучета'='AccountingFlag'; 'признакучёта'='AccountingFlag'
'признакучета'='AccountingFlag'; 'признакучёта'='AccountingFlag';
# Ссылочные формы (тип ссылки вместо объекта метаданных): в MDObjectRef-пути нужен ОБЪЕКТ, т.е.
# "CatalogRef.Валюты" → "Catalog.Валюты". Вид метаданных, оканчивающийся на Ref, не существует,
# поэтому схлопывание однозначно. В ТИПАХ реквизитов запись CatalogRef.X верна — там эта мапа не применяется.
'catalogref'='Catalog'; 'documentref'='Document'; 'enumref'='Enum';
'chartofaccountsref'='ChartOfAccounts'; 'chartofcharacteristictypesref'='ChartOfCharacteristicTypes';
'chartofcalculationtypesref'='ChartOfCalculationTypes'; 'exchangeplanref'='ExchangePlan';
'businessprocessref'='BusinessProcess'; 'taskref'='Task';
'справочникссылка'='Catalog'; 'документссылка'='Document'; 'перечислениессылка'='Enum';
'плансчетовссылка'='ChartOfAccounts'; 'планвидовхарактеристикссылка'='ChartOfCharacteristicTypes';
'планвидоврасчетассылка'='ChartOfCalculationTypes'; 'планвидоврасчётассылка'='ChartOfCalculationTypes';
'планобменассылка'='ExchangePlan'; 'бизнеспроцессссылка'='BusinessProcess'; 'задачассылка'='Task'
}
# $defaultRoot — корень для ГОЛОГО имени без точки (напр. owners: "Валюты" → "Catalog.Валюты").
# Без него голое имя возвращается как есть (прежнее поведение вызывающих без подстановки).
function Normalize-MDObjectRef {
param([string]$ref)
if (-not $ref -or -not $ref.Contains('.')) { return $ref }
param([string]$ref, [string]$defaultRoot)
if (-not $ref) { return $ref }
if (-not $ref.Contains('.')) {
if ($defaultRoot) { return "$defaultRoot.$ref" }
return $ref
}
$parts = $ref -split '\.'
for ($k = 0; $k -lt $parts.Count; $k += 2) {
$t = $script:mdRefRoots[$parts[$k].ToLower()]
@@ -1497,7 +1514,7 @@ function Emit-BasedOn {
$arr = @($items | Where-Object { $_ })
if ($arr.Count -eq 0) { X "$indent<BasedOn/>"; return }
X "$indent<BasedOn>"
foreach ($it in $arr) { X "$indent`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml "$it")</xr:Item>" }
foreach ($it in $arr) { X "$indent`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml (Normalize-MDObjectRef "$it"))</xr:Item>" }
X "$indent</BasedOn>"
}
@@ -2258,8 +2275,7 @@ function Emit-CatalogProperties {
if ($def.owners -and $def.owners.Count -gt 0) {
X "$i<Owners>"
foreach ($ownerRef in $def.owners) {
$fullRef = if ("$ownerRef" -match '\.') { "$ownerRef" } else { "Catalog.$ownerRef" }
X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$fullRef</xr:Item>"
X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml (Normalize-MDObjectRef "$ownerRef" 'Catalog'))</xr:Item>"
}
X "$i</Owners>"
} else {
@@ -2410,7 +2426,7 @@ function Emit-DocumentProperties {
}
if ($regRecords.Count -gt 0) {
X "$i<RegisterRecords>"
foreach ($rr in $regRecords) { X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$rr</xr:Item>" }
foreach ($rr in $regRecords) { X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml (Normalize-MDObjectRef "$rr"))</xr:Item>" }
X "$i</RegisterRecords>"
} else {
X "$i<RegisterRecords/>"
@@ -3524,7 +3540,7 @@ function Emit-ChartOfCalculationTypesProperties {
$baseTypes = @(); if ($def.baseCalculationTypes) { $baseTypes = @($def.baseCalculationTypes | ForEach-Object { Resolve-TypePrefixSyn "$_" }) }
if ($baseTypes.Count -gt 0) {
X "$i<BaseCalculationTypes>"
foreach ($bt in $baseTypes) { X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml $bt)</xr:Item>" }
foreach ($bt in $baseTypes) { X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml (Normalize-MDObjectRef "$bt"))</xr:Item>" }
X "$i</BaseCalculationTypes>"
} else { X "$i<BaseCalculationTypes/>" }
$actionPeriodUse = if ($def.actionPeriodUse -eq $true) { "true" } else { "false" }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-compile v1.66 — Compile 1C metadata object from JSON
# meta-compile v1.67 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -822,11 +822,26 @@ md_ref_roots = {
'табличнаячасть': 'TabularSection', 'реквизит': 'Attribute', 'измерение': 'Dimension', 'ресурс': 'Resource',
'стандартныйреквизит': 'StandardAttribute', 'значениеперечисления': 'EnumValue', 'команда': 'Command',
'признакучета': 'AccountingFlag', 'признакучёта': 'AccountingFlag',
# Ссылочные формы (тип ссылки вместо объекта метаданных): в MDObjectRef-пути нужен ОБЪЕКТ, т.е.
# "CatalogRef.Валюты" → "Catalog.Валюты". Вид метаданных, оканчивающийся на Ref, не существует,
# поэтому схлопывание однозначно. В ТИПАХ реквизитов запись CatalogRef.X верна — там мапа не применяется.
'catalogref': 'Catalog', 'documentref': 'Document', 'enumref': 'Enum',
'chartofaccountsref': 'ChartOfAccounts', 'chartofcharacteristictypesref': 'ChartOfCharacteristicTypes',
'chartofcalculationtypesref': 'ChartOfCalculationTypes', 'exchangeplanref': 'ExchangePlan',
'businessprocessref': 'BusinessProcess', 'taskref': 'Task',
'справочникссылка': 'Catalog', 'документссылка': 'Document', 'перечислениессылка': 'Enum',
'плансчетовссылка': 'ChartOfAccounts', 'планвидовхарактеристикссылка': 'ChartOfCharacteristicTypes',
'планвидоврасчетассылка': 'ChartOfCalculationTypes', 'планвидоврасчётассылка': 'ChartOfCalculationTypes',
'планобменассылка': 'ExchangePlan', 'бизнеспроцессссылка': 'BusinessProcess', 'задачассылка': 'Task',
}
def normalize_md_object_ref(ref):
if not ref or '.' not in ref:
def normalize_md_object_ref(ref, default_root=None):
"""default_root — корень для ГОЛОГО имени без точки (owners: "Валюты""Catalog.Валюты").
Без него голое имя возвращается как есть (прежнее поведение вызывающих без подстановки)."""
if not ref:
return ref
if '.' not in ref:
return f'{default_root}.{ref}' if default_root else ref
parts = ref.split('.')
for k in range(0, len(parts), 2):
t = md_ref_roots.get(parts[k].lower())
@@ -1587,7 +1602,7 @@ def emit_based_on(indent, items):
return
X(f'{indent}<BasedOn>')
for it in arr:
X(f'{indent}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(str(it))}</xr:Item>')
X(f'{indent}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(normalize_md_object_ref(str(it)))}</xr:Item>')
X(f'{indent}</BasedOn>')
# --- Параметры/связи выбора (порт из form-compile) ---
@@ -2288,8 +2303,7 @@ def emit_catalog_properties(indent):
if owners:
X(f'{i}<Owners>')
for owner_ref in owners:
full_ref = owner_ref if '.' in str(owner_ref) else f'Catalog.{owner_ref}'
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{full_ref}</xr:Item>')
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(normalize_md_object_ref(str(owner_ref), "Catalog"))}</xr:Item>')
X(f'{i}</Owners>')
else:
X(f'{i}<Owners/>')
@@ -2429,7 +2443,7 @@ def emit_document_properties(indent):
if reg_records:
X(f'{i}<RegisterRecords>')
for rr in reg_records:
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{rr}</xr:Item>')
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(normalize_md_object_ref(str(rr)))}</xr:Item>')
X(f'{i}</RegisterRecords>')
else:
X(f'{i}<RegisterRecords/>')
@@ -3483,7 +3497,7 @@ def emit_chart_of_calculation_types_properties(indent):
if base_types:
X(f'{i}<BaseCalculationTypes>')
for bt in base_types:
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(bt)}</xr:Item>')
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(normalize_md_object_ref(bt))}</xr:Item>')
X(f'{i}</BaseCalculationTypes>')
else:
X(f'{i}<BaseCalculationTypes/>')
@@ -1,14 +1,14 @@
{
"name": "InputByString / DataLockFields / BasedOn каталога",
"name": "InputByString / DataLockFields / BasedOn каталога (+нормализация MDObjectRef: CatalogRef./рус. → Catalog.)",
"input": {
"type": "Catalog",
"name": "ДоговорыКонтрагентов",
"owners": ["Catalog.Контрагенты"],
"owners": ["CatalogRef.Контрагенты"],
"codeLength": 11,
"descriptionLength": 100,
"inputByString": ["Код", "Наименование", "НомерДоговора"],
"dataLockFields": ["Организация", "Контрагент", "Владелец"],
"basedOn": ["Catalog.Контрагенты", "Document.ЗаказПоставщику"],
"basedOn": ["Справочник.Контрагенты", "DocumentRef.ЗаказПоставщику"],
"attributes": [
"Организация: CatalogRef.Организации",
"Контрагент: CatalogRef.Контрагенты",