fix(meta-compile,meta-edit): ссылочный тип корневым cfg:, как пишет платформа

Эмитился локальный xmlns:d5p1 на тот же URI, что уже объявлен в шапке файла.
Формально эквивалентно (значим URI, а не префикс), платформа принимала — но
первый же цикл «загрузить в базу → выгрузить» переписывал КАЖДЫЙ ссылочный тип
в cfg:, то есть давал diff-шум при неизменной семантике. Форма пришла из СКД,
где cfg: действительно не работает; в метаданных такого ограничения нет.

Радиус оказался втрое меньше прежней оценки: form-compile в него НЕ входил —
он уже писал cfg:, а все его d5p1 относятся к чужим пространствам
(txtedt/chart/geo/graphscheme/data-analysis), которых в корне нет и где
локальное объявление законно.

meta-edit берёт префикс из объявлений КОРНЯ правимого файла, а если URI там не
объявлен — остаётся на самодостаточной локальной форме. Это не перестраховка:
ссылочный тип живёт в ТЕКСТЕ узла, поэтому XML-слой про префикс не знает и сам
объявление не добавит — проверено, .NET на документе без xmlns:cfg молча пишет
неразрешимый префикс. Та же природа объясняет удалённый костыль в meta-edit.py:
lxml выбрасывал xmlns:d5p1 как «неиспользуемый», и его возвращали регуляркой
после сериализации.

Синхронно снята нормализация ref-префикса в раундтрип-харнесе (debug/, вне git):
она сводила cfg: и d5p1: к одному REF: и делала харнес слепым ровно к тому,
ради чего затевался переход.

Проверка:
- живой цикл через базу (db-create → db-load-xml → db-dump-xml): выгрузка
  платформы совпала с нашими исходниками ПОСТРОЧНО, расхождений ноль;
- корпусный раундтрип 4897 объектов уже БЕЗ маски префикса: match не просел
  (150/1640/151/2600/314/42), порядок ≠ 0, TOTAL diff lines 0;
- юнит-тесты 648/648 ps1, 645/648 py;
- 1С-сертификация meta-edit 17/17 на платформе 8.3.24;
- дрейф эталонов 48 файлов, в диффе нет ни одной строки кроме d5p1: → cfg:;
  skd-* не затронуты ни одним файлом (там cfg: не работает).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-06 16:28:47 +03:00
co-authored by Claude Opus 5
parent a3aa8fe87c
commit 683e370938
52 changed files with 172 additions and 127 deletions
@@ -1,4 +1,4 @@
# meta-compile v1.84 — Compile 1C metadata object from JSON
# meta-compile v1.85 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -760,9 +760,15 @@ function Emit-TypeContent {
return
}
# Reference types — use local xmlns declaration for 1C compatibility
# Ссылочные типы — корневой cfg:, как пишет платформа. Раньше здесь объявлялся
# ЛОКАЛЬНЫЙ xmlns:d5p1 на тот же URI, что уже объявлен в шапке ($script:xmlnsDecl):
# формально эквивалентно (значим URI, не префикс) и платформой принималось, но
# первый же цикл «загрузить в базу → выгрузить» переписывал каждый ссылочный тип
# в cfg: — то есть давал diff-шум на ровном месте. Форма пришла из СКД, где cfg:
# действительно не работает; в метаданных такого ограничения нет.
# NB: локальная xmlns остаётся законной для ЧУЖИХ пространств — см. $script:typeNamespaceMap.
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$') {
X "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$typeStr</v8:Type>"
X "$indent<v8:Type>cfg:$typeStr</v8:Type>"
return
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-compile v1.84 — Compile 1C metadata object from JSON
# meta-compile v1.85 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -806,10 +806,16 @@ def emit_type_content(indent, type_str):
X(f'{indent}<v8:Type>cfg:{type_str}</v8:Type>')
return
# Reference types — use local xmlns declaration for 1C compatibility
# Ссылочные типы — корневой cfg:, как пишет платформа. Раньше здесь объявлялся
# ЛОКАЛЬНЫЙ xmlns:d5p1 на тот же URI, что уже объявлен в шапке: формально
# эквивалентно (значим URI, не префикс) и платформой принималось, но первый же
# цикл «загрузить в базу → выгрузить» переписывал каждый ссылочный тип в cfg: —
# то есть давал diff-шум на ровном месте. Форма пришла из СКД, где cfg:
# действительно не работает; в метаданных такого ограничения нет.
# NB: локальная xmlns остаётся законной для ЧУЖИХ пространств — см. type_namespace_map.
m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$', type_str)
if m:
X(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{type_str}</v8:Type>')
X(f'{indent}<v8:Type>cfg:{type_str}</v8:Type>')
return
# Fallback
X(f'{indent}<v8:Type>{type_str}</v8:Type>')
+24 -3
View File
@@ -1,4 +1,4 @@
# meta-edit v1.29 — Edit existing 1C metadata object XML
# meta-edit v1.30 — Edit existing 1C metadata object XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -313,6 +313,17 @@ function Info($msg) {
# ============================================================
$root = $script:xmlDoc.DocumentElement
# Префикс пространства current-config, объявленный в КОРНЕ файла (у платформы — cfg).
# Ищем по объявлениям корня, а не через GetPrefixOfNamespace: ссылочный тип живёт в
# ТЕКСТЕ узла, поэтому XML-слой этот префикс не отслеживает. $null = корень URI не
# объявляет → эмиттер остаётся на самодостаточной локальной форме.
$script:cfgUri = 'http://v8.1c.ru/8.1/data/enterprise/current-config'
$script:cfgPrefix = $null
foreach ($a in $root.Attributes) {
if ($a.Prefix -eq 'xmlns' -and $a.Value -eq $script:cfgUri) { $script:cfgPrefix = $a.LocalName; break }
}
if ($root.LocalName -ne "MetaDataObject") {
Write-Error "Root element must be MetaDataObject, got: $($root.LocalName)"
exit 1
@@ -557,9 +568,19 @@ function Build-TypeContentXml {
return $sb.ToString().TrimEnd("`r","`n")
}
# Reference types — use local xmlns declaration for 1C compatibility
# Ссылочные типы — префиксом, объявленным в КОРНЕ файла (у платформы это cfg).
# Раньше здесь всегда объявлялся локальный xmlns:d5p1 на тот же URI, что уже есть
# в шапке: платформа принимала, но при цикле «загрузить в базу → выгрузить»
# переписывала каждый ссылочный тип в cfg: — diff-шум на ровном месте.
# Если корень URI не объявляет (файл не от платформы), остаёмся на самодостаточной
# локальной форме: префикс тут — ТЕКСТ узла, XML-слой про него не знает и сам
# объявление не добавит, так что иначе получился бы неразрешимый префикс.
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$') {
$sb.AppendLine("$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$typeStr</v8:Type>") | Out-Null
if ($script:cfgPrefix) {
$sb.AppendLine("$indent<v8:Type>$($script:cfgPrefix):$typeStr</v8:Type>") | Out-Null
} else {
$sb.AppendLine("$indent<v8:Type xmlns:d5p1=`"$script:cfgUri`">d5p1:$typeStr</v8:Type>") | Out-Null
}
return $sb.ToString().TrimEnd("`r","`n")
}
+22 -10
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-edit v1.29 — Edit existing 1C metadata object XML
# meta-edit v1.30 — Edit existing 1C metadata object XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -198,6 +198,9 @@ V8_NS = "http://v8.1c.ru/8.1/data/core"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
XS_NS = "http://www.w3.org/2001/XMLSchema"
CFG_NS = "http://v8.1c.ru/8.1/data/enterprise/current-config"
# Префикс current-config, объявленный в КОРНЕ правимого файла (у платформы — cfg).
# None = корень его не объявляет → эмиттер ссылочных типов остаётся на локальной форме.
cfg_prefix = None
NSMAP_WRAPPER = {
None: MD_NS,
@@ -525,14 +528,23 @@ def build_type_content_xml(indent, type_str):
lines.append(f"{indent}<v8:TypeSet>cfg:DefinedType.{dt_name}</v8:TypeSet>")
return "\r\n".join(lines)
# Reference types — use local xmlns declaration for 1C compatibility
# Ссылочные типы — префиксом, объявленным в КОРНЕ файла (у платформы это cfg).
# Раньше здесь всегда объявлялся локальный xmlns:d5p1 на тот же URI, что уже есть
# в шапке: платформа принимала, но при цикле «загрузить в базу → выгрузить»
# переписывала каждый ссылочный тип в cfg: — diff-шум на ровном месте.
# Если корень URI не объявляет (файл не от платформы), остаёмся на самодостаточной
# локальной форме: префикс тут — ТЕКСТ узла, XML-слой про него не знает и сам
# объявление не добавит, так что иначе получился бы неразрешимый префикс.
m = re.match(
r"^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|"
r"ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$",
type_str,
)
if m:
lines.append(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{type_str}</v8:Type>')
if cfg_prefix:
lines.append(f'{indent}<v8:Type>{cfg_prefix}:{type_str}</v8:Type>')
else:
lines.append(f'{indent}<v8:Type xmlns:d5p1="{CFG_NS}">d5p1:{type_str}</v8:Type>')
return "\r\n".join(lines)
# Fallback
@@ -2986,13 +2998,9 @@ def save_xml(tree, path):
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
# Fix d5p1 namespace declarations stripped by lxml (it treats them as unused
# because d5p1: appears only in text content, not in element/attribute names)
xml_bytes = re.sub(
b'(<v8:Type)(?! xmlns:d5p1)(>d5p1:)',
b'\\1 xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config"\\2',
xml_bytes
)
# Костыль, возвращавший xmlns:d5p1 (lxml выбрасывал его как неиспользуемый, ведь
# префикс встречается только в тексте узла), удалён вместе с переходом на корневой
# cfg: — объявление теперь берётся из шапки самого файла и не требует починки.
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
if style is None or style["bom"]:
@@ -3198,6 +3206,10 @@ def main():
xml_tree = etree.parse(resolved_path, xml_parser)
xml_root = xml_tree.getroot()
# Префикс current-config берём из объявлений корня — им и пишем ссылочные типы.
global cfg_prefix
cfg_prefix = next((p for p, u in (xml_root.nsmap or {}).items() if u == CFG_NS and p), None)
# --- Detect object type ---
if localname(xml_root) != "MetaDataObject":
die(f"Root element must be MetaDataObject, got: {localname(xml_root)}")