fix(xdto-compile,xdto-info,xdto-edit): правки по итогам прогона на субагентах

Четырём субагентам выданы реалистичные задачи по песочнице, навыки в
формулировках не назывались. Разбор — в debug/xdto/FINDINGS.md, §13.

ГЛАВНОЕ — дефект компилятора. При уплощении вложенного xs:choice ветки
оставались обязательными: схема «самовывоз ИЛИ адрес доставки» давала пакет,
требующий заполнить оба, и ни один реальный документ в него не ложился.
Компилятор предупреждал о потере выбора, но молчал о последствии, а валидатор
показывал «0 ошибок, 0 предупреждений» — структурно пакет корректен,
семантически мёртв. Теперь ветки становятся необязательными (единственное
уплощение, оставляющее тип заполнимым), предупреждение называет их поимённо.
Корневой xs:choice не затронут: он отображается в ordered="false".

xdto-edit: -Value "@файл" по конвенции skd-edit — на кавычках при инлайновой
передаче XSD споткнулись двое агентов из четырёх, причём сырой LoadXml уводил
чинить схему вместо транспорта. При сбое разбора теперь понятное сообщение.

xdto-info: поиск, законно ничего не нашедший, падал throw'ом со стектрейсом и
читался как поломка инструмента — теперь строка и exit 1. Блок «Создание»
покрывал только корневой тип, хотя вся реальная работа в XDTO — вложенные и
анонимные типы; добавлены рецепты по факту наличия. Отсутствие раздела «Точки
входа» было неоднозначным — теперь явная строка. Новый -RequiredOnly даёт
скелет «заполни обязательное»: необязательный объект уходит вместе с поддеревом.
По умолчанию выключен — иначе список читался бы как полный.

xdto-validate: предупреждение про anyType описывало историю («платформа заменяет
при импорте»), хотя в файле уже зафиксирован anyType; сначала состояние, потом
происхождение.

Проверено: 40 тестов на обоих рантаймах, round-trip 760/760, валидатор
0 ложных срабатываний на корпусе.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-26 13:44:46 +03:00
co-authored by Claude Opus 5
parent 1a1bbbac6f
commit 6486f433de
23 changed files with 590 additions and 36 deletions
@@ -493,24 +493,38 @@ function Resolve-Group([System.Xml.XmlElement]$el, [string]$kind) {
# Модель XDTO знает только плоский список свойств: вложенные частицы уплощаются.
# Каждое уплощение — предупреждение, потому что меняется смысл схемы.
function Collect-Particle([System.Xml.XmlElement]$particle, $elemList, [ref]$isOpen, [string]$typeName, [int]$depth) {
function Collect-Particle([System.Xml.XmlElement]$particle, $elemList, [ref]$isOpen, [string]$typeName, [int]$depth, [bool]$optionalize = $false) {
if ($depth -gt 20) { return }
foreach ($c in $particle.ChildNodes) {
if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element -or $c.NamespaceURI -ne $XS_NS) { continue }
switch ($c.get_LocalName()) {
"element" { [void]$elemList.Add((Build-Property $c $false)) }
"element" {
$prop = Build-Property $c $false
# Ветка уплощённого xs:choice обязана стать необязательной: иначе
# «одно из двух» превращается в «оба сразу», и тип нельзя заполнить
if ($optionalize) { Set-AttrValue $prop "lowerBound" "0" }
[void]$elemList.Add($prop)
}
"any" { $isOpen.Value = $true }
"sequence" {
Warn "$typeName : вложенная xs:sequence уплощена — модель XDTO хранит плоский список свойств"
Collect-Particle $c $elemList $isOpen $typeName ($depth + 1)
Collect-Particle $c $elemList $isOpen $typeName ($depth + 1) $optionalize
}
"choice" {
Warn "$typeName : вложенная xs:choice уплощена в последовательность — выбор одного из вариантов не сохранён"
Collect-Particle $c $elemList $isOpen $typeName ($depth + 1)
$branches = @()
foreach ($b in $c.ChildNodes) {
if ($b.NodeType -eq [System.Xml.XmlNodeType]::Element -and $b.NamespaceURI -eq $XS_NS -and $b.HasAttribute("name")) {
$branches += $b.GetAttribute("name")
}
}
$list = if ($branches.Count -gt 0) { " (" + ($branches -join ", ") + ")" } else { "" }
Warn ("$typeName : вложенная xs:choice уплощена — ветки$list сделаны необязательными. " +
"Выбор одного из вариантов не сохранён: модель не запретит заполнить сразу несколько или ни одного")
Collect-Particle $c $elemList $isOpen $typeName ($depth + 1) $true
}
"all" {
Warn "$typeName : xs:all трактуется как последовательность"
Collect-Particle $c $elemList $isOpen $typeName ($depth + 1)
Collect-Particle $c $elemList $isOpen $typeName ($depth + 1) $optionalize
}
"group" {
$g = Resolve-Group $c "group"
@@ -518,7 +532,7 @@ function Collect-Particle([System.Xml.XmlElement]$particle, $elemList, [ref]$isO
foreach ($gc in $g.ChildNodes) {
if ($gc.NodeType -eq [System.Xml.XmlNodeType]::Element -and $gc.NamespaceURI -eq $XS_NS -and
@("sequence", "choice", "all") -contains $gc.get_LocalName()) {
Collect-Particle $gc $elemList $isOpen $typeName ($depth + 1)
Collect-Particle $gc $elemList $isOpen $typeName ($depth + 1) $optionalize
}
}
} else {
@@ -541,7 +541,7 @@ def resolve_group(el, kind):
# Модель XDTO знает только плоский список свойств: вложенные частицы уплощаются.
# Каждое уплощение — предупреждение, потому что меняется смысл схемы.
def collect_particle(particle, elem_list, open_flag, type_name, depth):
def collect_particle(particle, elem_list, open_flag, type_name, depth, optionalize=False):
if depth > 20:
return
for c in particle:
@@ -549,25 +549,35 @@ def collect_particle(particle, elem_list, open_flag, type_name, depth):
continue
ln = local(c)
if ln == "element":
elem_list.append(build_property(c, False))
prop = build_property(c, False)
# Ветка уплощённого xs:choice обязана стать необязательной: иначе
# «одно из двух» превращается в «оба сразу», и тип нельзя заполнить
if optionalize:
set_attr_value(prop, "lowerBound", "0")
elem_list.append(prop)
elif ln == "any":
open_flag[0] = True
elif ln == "sequence":
warn(type_name + " : вложенная xs:sequence уплощена — модель XDTO хранит плоский список свойств")
collect_particle(c, elem_list, open_flag, type_name, depth + 1)
collect_particle(c, elem_list, open_flag, type_name, depth + 1, optionalize)
elif ln == "choice":
warn(type_name + " : вложенная xs:choice уплощена в последовательность — выбор одного из вариантов не сохранён")
collect_particle(c, elem_list, open_flag, type_name, depth + 1)
branches = [b.get("name") for b in c
if isinstance(b.tag, str) and etree.QName(b).namespace == XS_NS and b.get("name")]
lst = (" (" + ", ".join(branches) + ")") if branches else ""
warn(type_name + " : вложенная xs:choice уплощена — ветки" + lst + " сделаны необязательными. "
"Выбор одного из вариантов не сохранён: модель не запретит заполнить "
"сразу несколько или ни одного")
collect_particle(c, elem_list, open_flag, type_name, depth + 1, True)
elif ln == "all":
warn(type_name + " : xs:all трактуется как последовательность")
collect_particle(c, elem_list, open_flag, type_name, depth + 1)
collect_particle(c, elem_list, open_flag, type_name, depth + 1, optionalize)
elif ln == "group":
g = resolve_group(c, "group")
if g is not None:
for gc in g:
if isinstance(gc.tag, str) and etree.QName(gc).namespace == XS_NS \
and local(gc) in ("sequence", "choice", "all"):
collect_particle(gc, elem_list, open_flag, type_name, depth + 1)
collect_particle(gc, elem_list, open_flag, type_name, depth + 1, optionalize)
else:
warn(type_name + " : не найдена группа " + str(c.get("ref")) + " — её свойства в пакет не попали")
if ln in ("sequence", "choice", "all", "group"):