refactor(skills): регистрация в ChildObjects — именованная функция под анти-дрейфом

Три навыка делали одну работу тремя инлайновыми копиями в main(), и одна копия
молча разъехалась: role-compile.py рапортовал об успехе, не тронув файл. Это
ровно тот класс, от которого заведён check-inline-drift.mjs, но в реестр
регистрацию было не записать — он работает по именованным функциям.

Регистрация вынесена в Register-InChildObjects / register_in_childobjects
с контрактом (родительский XML, тег родителя, тег потомка, имя) и исходом
added | already | no-childobj | no-config. Печать сообщений осталась на
вызывающей стороне: тексты у навыков разные, и сведение их меняло бы вывод.

Эталон — meta-compile, форма функции задана на .ps1. role-compile берёт его
тело копией, и гард это подтверждает. subsystem-compile идёт отдельным
вариантом с обоснованием: родителем бывает вложенный Subsystem.xml
произвольной глубины, поэтому отступ он берёт из документа, а запись
дописывает в конец блока — фиксированные три табуляции там неверны,
и группировать по типу нечего.

Вынос поведенчески нейтрален: 837 кейсов зелёные на обоих рантаймах,
снэпшоты не дрейфовали, матрица «навык × порт × изломанная форма
Configuration.xml» даёт те же исходы, а порты — байт-в-байт одинаковый файл.
Попутно из subsystem-compile.py убран мёртвый ET.SubElement перед pass:
дерево мутировалось и выбрасывалось, правка идёт по сырому тексту.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-19 11:52:40 +03:00
co-authored by Claude Opus 5
parent 2b0ac3ea75
commit 1c63d9ddcc
7 changed files with 458 additions and 419 deletions
@@ -1,4 +1,4 @@
# subsystem-compile v1.25 — Create 1C subsystem from JSON definition (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# subsystem-compile v1.26 — Create 1C subsystem from JSON definition
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -600,9 +600,106 @@ if ($children.Count -gt 0) {
}
# --- 6. Register in parent ---
# Регистрация объекта в <ChildObjects> родительского XML. Вариант семьи: отступ берётся
# из самого документа, а запись дописывается в конец блока. Отличие от эталона
# (meta-compile) осознанное: родителем бывает вложенный Subsystem.xml произвольной
# глубины, где фиксированные три табуляции неверны, а группировать записи по типу
# внутри подсистемы нечего — потомок там всегда один и тот же.
# Реестр семьи: tests/skills/check-inline-drift.mjs.
# Возвращает исход: added | already | no-childobj | no-config.
function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [string]$ChildTag, [string]$ChildName) {
if (-not (Test-Path $ParentXmlPath)) { return "no-config" }
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $true
$doc.Load($ParentXmlPath)
$ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$childObjects = $doc.SelectSingleNode("//md:$ParentTag/md:ChildObjects", $ns)
if (-not $childObjects) { return "no-childobj" }
# Check for self-closing tag
$isSelfClosing = (-not $childObjects.HasChildNodes) -or ($childObjects.IsEmpty)
# Check if already registered
foreach ($child in $childObjects.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $ChildTag -and $child.InnerText -eq $ChildName) {
return "already"
}
}
$newEl = $doc.CreateElement($ChildTag, "http://v8.1c.ru/8.3/MDClasses")
$newEl.InnerText = $ChildName
if ($isSelfClosing) {
# Expand self-closing tag
$parentIndent = ""
$prev = $childObjects.PreviousSibling
if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) {
if ($prev.Value -match '(\t+)$') { $parentIndent = $Matches[1] }
}
$childIndent = "$parentIndent`t"
$ws1 = $doc.CreateWhitespace("`r`n$childIndent")
$ws2 = $doc.CreateWhitespace("`r`n$parentIndent")
$childObjects.AppendChild($ws1) | Out-Null
$childObjects.AppendChild($newEl) | Out-Null
$childObjects.AppendChild($ws2) | Out-Null
} else {
# Insert before trailing whitespace
$childIndent = "`t`t`t"
foreach ($child in $childObjects.ChildNodes) {
if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') {
if ($child.Value -match '^\r?\n(\t+)') { $childIndent = $Matches[1]; break }
}
}
$trailing = $childObjects.LastChild
$ws = $doc.CreateWhitespace("`r`n$childIndent")
if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) {
$childObjects.InsertBefore($ws, $trailing) | Out-Null
$childObjects.InsertBefore($newEl, $trailing) | Out-Null
} else {
$childObjects.AppendChild($ws) | Out-Null
$childObjects.AppendChild($newEl) | Out-Null
}
}
# Save parent XML
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
$settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$doc.Save($writer)
$writer.Flush(); $writer.Close()
$bytes = $memStream.ToArray()
$memStream.Close()
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $ParentXmlPath) -and ([System.IO.File]::ReadAllText($ParentXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($ParentXmlPath, $text, (New-Object System.Text.UTF8Encoding($true)))
return "added"
}
$parentXmlPath = $null
$parentTag = "Configuration"
if ($Parent) {
$parentXmlPath = $Parent
$parentTag = "Subsystem"
} else {
$configXml = Join-Path $OutputDir "Configuration.xml"
if (Test-Path $configXml) {
@@ -610,103 +707,12 @@ if ($Parent) {
}
}
if ($parentXmlPath -and (Test-Path $parentXmlPath)) {
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $true
$doc.Load($parentXmlPath)
$ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
# Find ChildObjects
$childObjects = $null
if ($Parent) {
$childObjects = $doc.SelectSingleNode("//md:Subsystem/md:ChildObjects", $ns)
} else {
$childObjects = $doc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
}
if ($childObjects) {
# Check for self-closing tag
$isSelfClosing = (-not $childObjects.HasChildNodes) -or ($childObjects.IsEmpty)
# Check if already registered
$alreadyExists = $false
foreach ($child in $childObjects.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem" -and $child.InnerText -eq $objName) {
$alreadyExists = $true
break
}
}
if (-not $alreadyExists) {
$newEl = $doc.CreateElement("Subsystem", "http://v8.1c.ru/8.3/MDClasses")
$newEl.InnerText = $objName
if ($isSelfClosing) {
# Expand self-closing tag
$parentIndent = ""
$prev = $childObjects.PreviousSibling
if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) {
if ($prev.Value -match '(\t+)$') { $parentIndent = $Matches[1] }
}
$childIndent = "$parentIndent`t"
$ws1 = $doc.CreateWhitespace("`r`n$childIndent")
$ws2 = $doc.CreateWhitespace("`r`n$parentIndent")
$childObjects.AppendChild($ws1) | Out-Null
$childObjects.AppendChild($newEl) | Out-Null
$childObjects.AppendChild($ws2) | Out-Null
} else {
# Insert before trailing whitespace
$childIndent = "`t`t`t"
foreach ($child in $childObjects.ChildNodes) {
if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') {
if ($child.Value -match '^\r?\n(\t+)') { $childIndent = $Matches[1]; break }
}
}
$trailing = $childObjects.LastChild
$ws = $doc.CreateWhitespace("`r`n$childIndent")
if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) {
$childObjects.InsertBefore($ws, $trailing) | Out-Null
$childObjects.InsertBefore($newEl, $trailing) | Out-Null
} else {
$childObjects.AppendChild($ws) | Out-Null
$childObjects.AppendChild($newEl) | Out-Null
}
}
# Save parent XML
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
$settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$doc.Save($writer)
$writer.Flush(); $writer.Close()
$bytes = $memStream.ToArray()
$memStream.Close()
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $parentXmlPath) -and ([System.IO.File]::ReadAllText($parentXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($parentXmlPath, $text, $utf8Bom)
Write-Host "[OK] Registered in: $parentXmlPath"
} else {
Write-Host "[SKIP] Already registered in: $parentXmlPath"
}
} else {
Write-Host "[WARN] ChildObjects not found in: $parentXmlPath"
if ($parentXmlPath) {
switch (Register-InChildObjects $parentXmlPath $parentTag "Subsystem" $objName) {
"added" { Write-Host "[OK] Registered in: $parentXmlPath" }
"already" { Write-Host "[SKIP] Already registered in: $parentXmlPath" }
"no-childobj" { Write-Host "[WARN] ChildObjects not found in: $parentXmlPath" }
"no-config" { Write-Host "[INFO] No parent XML to register in" }
}
} else {
Write-Host "[INFO] No parent XML to register in"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# subsystem-compile v1.25 — Create 1C subsystem from JSON definition (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# subsystem-compile v1.26 — Create 1C subsystem from JSON definition
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -388,6 +388,60 @@ def write_child_subsystem_stub(child_path, child_name, format_version):
write_utf8_bom(child_path, '\r\n'.join(lines))
def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name):
"""Регистрация объекта в <ChildObjects> родительского XML.
Вариант семьи: отступ берётся из самого документа, а запись дописывается в конец
блока. Отличие от эталона (meta-compile) осознанное: родителем бывает вложенный
Subsystem.xml произвольной глубины, где фиксированные три табуляции неверны,
а группировать записи по типу внутри подсистемы нечего — потомок там всегда один.
Реестр семьи: tests/skills/check-inline-drift.mjs.
Возвращает исход: added | already | no-childobj | no-config.
"""
if not os.path.exists(parent_xml_path):
return 'no-config'
# newline='' => без трансляции переводов строк: иначе CRLF молча схлопнется
# в LF при чтении и файл будет переписан в LF.
with open(parent_xml_path, 'r', encoding='utf-8-sig', newline='') as f:
raw_text = f.read()
eol = detect_eol(raw_text)
doc = ET.ElementTree(ET.fromstring(raw_text))
root = doc.getroot()
md_ns = 'http://v8.1c.ru/8.3/MDClasses'
# Find ChildObjects
child_objects = None
for holder in root.iter(f'{{{md_ns}}}{parent_tag}'):
child_objects = holder.find(f'{{{md_ns}}}ChildObjects')
break
if child_objects is None:
return 'no-childobj'
for child in child_objects:
if child.tag == f'{{{md_ns}}}{child_tag}' and child.text == child_name:
return 'already'
# Правку ведём по сырому тексту, а не сериализацией ET: она не сохраняет отступы
# и теряет xmlns, объявленные только внутри значений атрибутов (#38).
entry = f'<{child_tag}>{esc_xml_text(child_name)}</{child_tag}>'
if '<ChildObjects/>' in raw_text:
replacement = '<ChildObjects>' + eol + f'\t\t\t{entry}' + eol + '\t\t</ChildObjects>'
raw_text = raw_text.replace('<ChildObjects/>', replacement, 1)
elif '</ChildObjects>' in raw_text:
# Отступ вставки берём у закрывающего тега +1 уровень: подстановка
# по голому '</ChildObjects>' удваивала бы уже присутствующий отступ
# строки (получалось 5 табов вместо 3 — PS-порт через DOM даёт 3).
raw_text = re.sub(r'([ \t]*)</ChildObjects>',
lambda m: m.group(1) + '\t' + entry + eol + m.group(1) + '</ChildObjects>',
raw_text, count=1)
write_utf8_bom(parent_xml_path, raw_text)
return 'added'
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -656,71 +710,25 @@ def main():
# --- 5. Register in parent ---
parent_xml_path = None
parent_tag = 'Configuration'
if parent:
parent_xml_path = parent
parent_tag = 'Subsystem'
else:
config_xml = os.path.join(output_dir, 'Configuration.xml')
if os.path.exists(config_xml):
parent_xml_path = config_xml
if parent_xml_path and os.path.exists(parent_xml_path):
# newline='' => без трансляции переводов строк: иначе CRLF молча схлопнется
# в LF при чтении и файл будет переписан в LF.
with open(parent_xml_path, 'r', encoding='utf-8-sig', newline='') as f:
raw_text = f.read()
eol = detect_eol(raw_text)
doc = ET.ElementTree(ET.fromstring(raw_text))
root = doc.getroot()
md_ns = 'http://v8.1c.ru/8.3/MDClasses'
# Find ChildObjects
child_objects = None
if parent:
for sub in root.iter(f'{{{md_ns}}}Subsystem'):
child_objects = sub.find(f'{{{md_ns}}}ChildObjects')
break
else:
for cfg in root.iter(f'{{{md_ns}}}Configuration'):
child_objects = cfg.find(f'{{{md_ns}}}ChildObjects')
break
if child_objects is not None:
# Check if already registered
already_exists = False
for child in child_objects:
if child.tag == f'{{{md_ns}}}Subsystem' and child.text == obj_name:
already_exists = True
break
if not already_exists:
new_el = ET.SubElement(child_objects, f'{{{md_ns}}}Subsystem')
new_el.text = obj_name
# Re-serialize with whitespace preservation via raw text manipulation instead
# Since ElementTree doesn't preserve whitespace well, use regex-based insertion
# Find </ChildObjects> or <ChildObjects/> and inject
pass # Fall through to raw text approach below
if not already_exists:
# Use raw text manipulation to preserve formatting
if '<ChildObjects/>' in raw_text:
replacement = ('<ChildObjects>' + eol + f'\t\t\t<Subsystem>{esc_xml_text(obj_name)}</Subsystem>' + eol + '\t\t</ChildObjects>')
raw_text = raw_text.replace('<ChildObjects/>', replacement, 1)
elif '</ChildObjects>' in raw_text:
# Отступ вставки берём у закрывающего тега +1 уровень: подстановка
# по голому '</ChildObjects>' удваивала бы уже присутствующий отступ
# строки (получалось 5 табов вместо 3 — PS-порт через DOM даёт 3).
raw_text = re.sub(r'([ \t]*)</ChildObjects>',
lambda m: m.group(1) + '\t' + f'<Subsystem>{esc_xml_text(obj_name)}</Subsystem>' + eol + m.group(1) + '</ChildObjects>',
raw_text, count=1)
write_utf8_bom(parent_xml_path, raw_text)
print(f"[OK] Registered in: {parent_xml_path}")
else:
print(f"[SKIP] Already registered in: {parent_xml_path}")
else:
if parent_xml_path:
outcome = register_in_childobjects(parent_xml_path, parent_tag, 'Subsystem', obj_name)
if outcome == 'added':
print(f"[OK] Registered in: {parent_xml_path}")
elif outcome == 'already':
print(f"[SKIP] Already registered in: {parent_xml_path}")
elif outcome == 'no-childobj':
print(f"[WARN] ChildObjects not found in: {parent_xml_path}")
else:
print("[INFO] No parent XML to register in")
else:
print("[INFO] No parent XML to register in")