feat(skills): порядок объектов метаданных в ChildObjects

Навыки-создатели дописывали новый объект в конец группы своего вида, а стандарт
требует порядка по имени (АПК:1108). Замеры на 8 боевых выгрузках: 103 385 объектов
лежат по алфавиту, нарушают его ровно дописки в хвост. Стенд подтвердил, что
беспорядок вечен: платформа нормализует порядок ВИДОВ (возвращает канонический),
но порядок имён внутри вида не трогает.

Настройка newObjectPosition в .v8-project.json (end по умолчанию | byName) —
решение проекта, а не вызова: читают её meta-compile, role-compile, xdto-compile,
cfe-borrow и cf-edit add-childObject. Компаратор при этом константа, моделирующая
дерево Конфигуратора: ключ «ранг+символ» без культурных таблиц, одинаковый в обоих
портах на любой ОС. На корпусе он даёт 4 нарушения на 125 088 пар против 2866 у
ordinal-сравнения, которым cf-edit и cfe-borrow сортировали до сих пор.

Subsystem не упорядочивается автоматически нигде: пока подсистемы не перечислены
в <SubsystemsOrder>, порядок дерева задаёт порядок разделов в панели, а платформа
этот список сама не заводит.

Настройка не чинит накопленное, поэтому cf-edit получил операцию sort-childObjects
(вся конфигурация или названные виды). Она переставляет значения узлов, а не узлы,
поэтому диф — чистая перестановка строк. Имя вида принимается в любом регистре,
во множественном числе и по-русски; неизвестный вид — отказ со списком допустимых.

Попутно:
- PS-порты создателей переведены с DOM-сериализации на текстовую вставку: на
  выгрузке не в каноне Конфигуратора они переписывали заголовок без просьбы;
- PS-сторона семьи detect_xml_style/finalize_xml_bytes закрыта в cf-edit и
  cfe-borrow — правка чужого файла наследует его BOM/EOL/заголовок;
- xdto-compile присоединён к семье Register-InChildObjects вместо инлайн-копии;
- закрыт ложный успех в py subsystem-compile: <ChildObjects /> с пробелом ET
  разбирает, а текстовые ветки не находили — файл писался без вставки;
- cfe-borrow py не импортировал json, из-за чего резолвер молча возвращал end;
- cf-edit add-childObject ставил объект в конец блока, за пределы своей группы,
  когда видов старше в файле не было.

Проверки: 906/906 на PowerShell и 903/906 на Python, 10 гардов, верификация
эталонов платформой без падений, раундтрип отсортированной конфигурации на
8.3.24 и 8.3.27, сортировка боевой выгрузки ACC (1,4 МБ) и расширения из корпуса.

Задачу принёс PR #85; часть кода взята оттуда.

Co-Authored-By: Sergei Pleshanov <72200277+Abacadabras@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
This commit is contained in:
Nick Shirokov
2026-08-29 17:34:09 +03:00
co-authored by Sergei Pleshanov Claude Opus 5
parent 7409bacd47
commit c7b0dce151
302 changed files with 13595 additions and 388 deletions
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-compile v1.99 — Compile 1C metadata object from JSON
# meta-compile v1.100 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -5209,10 +5209,71 @@ if commands:
# 17. Register in Configuration.xml
# ---------------------------------------------------------------------------
def get_new_object_position(cfg_dir):
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
иначе корневое поле, иначе end. Значения: end после последнего объекта того же вида
(так дописывает Конфигуратор); byName по имени среди объектов того же вида.
Файл ищем от каталога конфигурации и лишь потом от cwd в отличие от support-guard:
настройка принадлежит выгрузке, а рабочим каталогом при вызове навыка почти всегда
оказывается чужой проект со своим .v8-project.json, и он перекрыл бы нужный.
configSrc считается от каталога .v8-project.json, как задокументировано в
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
try:
pj = _sg_find_v8project(os.path.abspath(cfg_dir or ".")) or _sg_find_v8project(os.getcwd())
if not pj:
return "end"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
proj_dir = os.path.dirname(pj)
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src and db.get("newObjectPosition"):
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
if str(proj.get("newObjectPosition") or "").lower() == "byname":
return "byName"
return "end"
except Exception:
return "end"
def compare_metadata_names(a, b):
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
Ключ пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
используются они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
keys = []
for name in (a, b):
parts = []
for ch in name.lower():
if ch == "ё":
ch = "е"
if ch.isdigit():
parts.append("1" + ch)
elif ch.isalpha():
parts.append("2" + ch)
else:
parts.append("0" + ch)
keys.append("".join(parts))
if keys[0] != keys[1]:
return -1 if keys[0] < keys[1] else 1
if a != b:
return -1 if a < b else 1
return 0
def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name):
"""Регистрация объекта в <ChildObjects> родительского XML.
Общая реализация: эталон meta-compile, копия role-compile.
Общая реализация: эталон meta-compile, копии role-compile, xdto-compile.
Реестр семьи: tests/skills/check-inline-drift.mjs.
Возвращает исход: added | already | no-childobj | no-config.
"""
@@ -5263,6 +5324,20 @@ def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name)
write_utf8_bom(parent_xml_path, new_content)
return 'added'
# byName: перед первым объектом того же вида, чьё имя больше нового. Subsystem — никогда:
# порядок подсистем в дереве задаёт порядок разделов в панели, пока их не перечислили в
# <SubsystemsOrder> файла Ext/CommandInterface.xml (платформа этот список сама не заводит).
if (child_tag != 'Subsystem'
and get_new_object_position(os.path.dirname(os.path.abspath(parent_xml_path))) == 'byName'):
line_rx = re.compile(rf'(?m)^([ \t]*)<{child_tag}>([^<]*)</{child_tag}>')
for m in line_rx.finditer(config_content, block.start(), block.end()):
if compare_metadata_names(m.group(2), child_name) > 0:
new_content = (config_content[:m.start()]
+ f'{m.group(1)}{entry}{eol}'
+ config_content[m.start():])
write_utf8_bom(parent_xml_path, new_content)
return 'added'
close_same = f'</{child_tag}>'
last_same = config_content.rfind(close_same, block.start(), block.end())
if last_same != -1: