fix(xdto-compile,xdto-validate): не терять конструкции молча; вычитать инструкции

xdto-compile терял свойства без единого слова: на реалистичной чужой схеме
из шести объявленных доезжало одно. Вложенные xs:sequence/xs:choice теперь
уплощаются (модель хранит плоский список), xs:all трактуется как
последовательность, xs:group и xs:attributeGroup раскрываются по ссылке —
и о каждом приближении навык пишет предупреждение. Молчаливая потеря — тот же
класс дефекта, что мы ловим у платформы, лечится так же: сообщением, не отказом.

xdto-validate получил проверки на грабли, найденные при разработке: порядок
элементов верхнего уровня (платформа отвергает пакет, не называя причины),
конфликты объявлений (name+ref, type+вложенный тип, тип без разновидности),
несовпадение рода базового типа, дубли имён свойств.

Новые правила прогнаны по всем 760 пакетам выгрузок: всё, что породила
платформа, валидно по определению, поэтому каждая ошибка там — ошибка правила.
Первый прогон дал 7, и все три класса оказались реальным поведением платформы:
length вместе с minLength/maxLength встречается, два пакета делят один
targetNamespace (Envelope и SOAP_Envelope_1_1 в БП), form="Text" называется
не только __content. Правила понижены до предупреждений либо сняты. Заодно
убран шум: предупреждение о неиспользуемом import срабатывало на четверти
корпуса — теперь только вместе с anyType, где оно и означает проблему.
Итог: 0 ошибок на корпусе, предупреждений 53 вместо 242.

Инструкции переписаны под читателя-исполнителя: убраны детали реализации
и наши мерки, каталог проверок валидатора (его вывод самодостаточен),
локальные пути в примерах заменены нейтральными. Таблица соответствий
XSD и справочник аннотаций вынесены в xdto-compile/xsd-reference.md.

Round-trip 760/760 сохранён, паритет PS/PY сохранён.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-25 21:48:48 +03:00
co-authored by Claude Opus 5
parent d05aef54b4
commit 7ca6dfa6b2
24 changed files with 1156 additions and 132 deletions
@@ -265,6 +265,13 @@ function Serialize-Node($node, [int]$depth, $inherited) {
# --- XSD reading helpers ---
# Предупреждения о том, что XSD выражает, а модель XDTO — нет. Молча ронять
# такие конструкции нельзя: пакет соберётся, а половина свойств исчезнет.
$script:warnings = New-Object System.Collections.ArrayList
function Warn([string]$msg) {
if (-not $script:warnings.Contains($msg)) { [void]$script:warnings.Add($msg) }
}
function XA([System.Xml.XmlElement]$el, [string]$name) {
if ($el.HasAttribute($name)) { return $el.GetAttribute($name) }
return $null
@@ -471,6 +478,63 @@ function Build-Property([System.Xml.XmlElement]$el, [bool]$isAttribute) {
# --- complexType -> objectType / typeDef(ObjectType) ---
# Разрешение xs:group / xs:attributeGroup по ссылке
$script:GROUPS = @{}
$script:ATTR_GROUPS = @{}
function Resolve-Group([System.Xml.XmlElement]$el, [string]$kind) {
$ref = XA $el "ref"
if (-not $ref) { return $null }
$q = Split-QName $el $ref
if (-not $q) { return $null }
$map = if ($kind -eq "group") { $script:GROUPS } else { $script:ATTR_GROUPS }
if ($map.ContainsKey($q.Local)) { return $map[$q.Local] }
return $null
}
# Модель XDTO знает только плоский список свойств: вложенные частицы уплощаются.
# Каждое уплощение — предупреждение, потому что меняется смысл схемы.
function Collect-Particle([System.Xml.XmlElement]$particle, $elemList, [ref]$isOpen, [string]$typeName, [int]$depth) {
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)) }
"any" { $isOpen.Value = $true }
"sequence" {
Warn "$typeName : вложенная xs:sequence уплощена — модель XDTO хранит плоский список свойств"
Collect-Particle $c $elemList $isOpen $typeName ($depth + 1)
}
"choice" {
Warn "$typeName : вложенная xs:choice уплощена в последовательность — выбор одного из вариантов не сохранён"
Collect-Particle $c $elemList $isOpen $typeName ($depth + 1)
}
"all" {
Warn "$typeName : xs:all трактуется как последовательность"
Collect-Particle $c $elemList $isOpen $typeName ($depth + 1)
}
"group" {
$g = Resolve-Group $c "group"
if ($g) {
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)
}
}
} else {
Warn "$typeName : не найдена группа $(XA $c 'ref') — её свойства в пакет не попали"
}
}
}
# Кратность на самой частице модель выразить не может
if (@("sequence", "choice", "all", "group") -contains $c.get_LocalName()) {
if ((XA $c "maxOccurs") -or (XA $c "minOccurs")) {
Warn "$typeName : кратность на вложенной частице (<xs:$($c.get_LocalName()) minOccurs/maxOccurs>) не выражается в модели XDTO"
}
}
}
}
# open / ordered / sequenced / abstract / mixed: выводим где выводимо,
# остальное приходит зеркалом xdto:
function Set-TypeFlags($node, [System.Xml.XmlElement]$ct, $isOpen, $choice) {
@@ -532,22 +596,48 @@ function Fill-ComplexType($node, [System.Xml.XmlElement]$ct) {
# Particle: xs:sequence (ordered) or xs:choice (ordered="false")
$seq = XFirst $body "sequence"
$cho = XFirst $body "choice"
$particle = if ($seq) { $seq } else { $cho }
$all = XFirst $body "all"
$grp = XFirst $body "group"
$particle = if ($seq) { $seq } elseif ($cho) { $cho } elseif ($all) { $all } else { $grp }
$isOpen = $false
# Порядок в XDTO: сначала form="Attribute", потом остальные (верно для 96.5%
# типов корпуса). Отклонения приходят зеркалом xdto:order.
$elemProps = New-Object System.Collections.ArrayList
$typeName = if ($ct.HasAttribute("name")) { $ct.GetAttribute("name") } else { "(анонимный тип)" }
if ($particle) {
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]$elemProps.Add((Build-Property $c $false)) }
"any" { $isOpen = $true }
}
$openRef = [ref]$isOpen
if ($all) {
Warn "$typeName : xs:all трактуется как последовательность"
}
if ($grp -and -not $seq -and -not $cho -and -not $all) {
# Корневая частица задана ссылкой на группу — раскрываем её содержимое
$g = Resolve-Group $grp "group"
if ($g) {
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 $elemProps $openRef $typeName 1
}
}
} else {
Warn "$typeName : не найдена группа $(XA $grp 'ref') — её свойства в пакет не попали"
}
} else {
Collect-Particle $particle $elemProps $openRef $typeName 0
}
$isOpen = $openRef.Value
}
foreach ($a in (XChildren $body "attribute")) { Add-Child $node (Build-Property $a $true) }
# xs:attributeGroup раскрываем по ссылке
foreach ($ag in (XChildren $body "attributeGroup")) {
$g = Resolve-Group $ag "attributeGroup"
if ($g) {
foreach ($a in (XChildren $g "attribute")) { Add-Child $node (Build-Property $a $true) }
} else {
Warn "Не найдена группа атрибутов $(XA $ag 'ref') — её атрибуты в пакет не попали"
}
}
foreach ($e in $elemProps) { Add-Child $node $e }
if ((XChildren $body "anyAttribute").Count -gt 0) { $isOpen = $true }
@@ -591,10 +681,37 @@ if ($ann) {
}
}
# Реестр глобальных групп — нужен до обхода, чтобы раскрывать ссылки
foreach ($node in $schema.ChildNodes) {
if ($node.NodeType -ne [System.Xml.XmlNodeType]::Element -or $node.NamespaceURI -ne $XS_NS) { continue }
$nm = XA $node "name"
if ($node.get_LocalName() -eq "group" -and $nm) { $script:GROUPS[$nm] = $node }
if ($node.get_LocalName() -eq "attributeGroup" -and $nm) { $script:ATTR_GROUPS[$nm] = $node }
}
# Конструкции XSD, которым в модели XDTO нет соответствия
foreach ($sg in $schema.SelectNodes("//*[local-name()='element'][@substitutionGroup]")) {
Warn "Подстановочные группы (substitutionGroup) не поддерживаются моделью XDTO — объявление $($sg.GetAttribute('name')) сохранено как обычное"
}
foreach ($idc in @("key", "keyref", "unique")) {
if ($schema.SelectNodes("//*[local-name()='$idc']").Count -gt 0) {
Warn "Ограничения целостности (xs:$idc) в модели XDTO не хранятся — отброшены"
}
}
if ($schema.SelectNodes("//*[local-name()='redefine']").Count -gt 0) {
Warn "xs:redefine не поддерживается — переопределения проигнорированы"
}
if ($schema.SelectNodes("//*[local-name()='include']").Count -gt 0) {
Warn "xs:include проигнорирован: модель XDTO разрешает зависимости только по namespace. Соберите включаемую схему отдельным пакетом и добавьте <xs:import>"
}
foreach ($node in $schema.ChildNodes) {
if ($node.NodeType -ne [System.Xml.XmlNodeType]::Element -or $node.NamespaceURI -ne $XS_NS) { continue }
switch ($node.get_LocalName()) {
"annotation" { }
"group" { }
"attributeGroup" { }
"notation" { }
"import" {
$n = New-Node "import"
Add-Attr $n "namespace" (XA $node "namespace")
@@ -765,6 +882,12 @@ Write-Host "✓ Пакет XDTO собран: $Name"
Write-Host " Namespace: $targetNs"
Write-Host " Типов: $typeCount"
Write-Host " Файлы: XDTOPackages/$Name.xml, XDTOPackages/$Name/Ext/Package.bin"
if ($script:warnings.Count -gt 0) {
Write-Host ""
Write-Host "Предупреждения ($($script:warnings.Count)) — конструкции XSD без точного соответствия в модели XDTO:"
foreach ($w in $script:warnings) { Write-Host " ! $w" }
Write-Host ""
}
switch ($regResult) {
"added" { Write-Host " Configuration.xml: <XDTOPackage>$Name</XDTOPackage> добавлен в ChildObjects" }
"already" { Write-Host " Configuration.xml: <XDTOPackage>$Name</XDTOPackage> уже зарегистрирован" }
@@ -297,6 +297,20 @@ def serialize_node(node, depth, inherited):
# ── XSD reading helpers ──────────────────────────────────────
# Предупреждения о том, что XSD выражает, а модель XDTO — нет. Молча ронять
# такие конструкции нельзя: пакет соберётся, а половина свойств исчезнет.
warnings_list = []
def warn(msg):
if msg not in warnings_list:
warnings_list.append(msg)
GROUPS = {}
ATTR_GROUPS = {}
def MA(el, name):
# xdto: mirror attribute — литеральное значение для Package.bin.
# Ищем по namespace, а не по строке префикса.
@@ -497,6 +511,54 @@ def build_property(el, is_attribute):
# ── complexType -> objectType / typeDef(ObjectType) ──────────
def resolve_group(el, kind):
ref = el.get("ref")
if not ref:
return None
q = split_qname(el, ref)
if not q:
return None
m = GROUPS if kind == "group" else ATTR_GROUPS
return m.get(q[1])
# Модель XDTO знает только плоский список свойств: вложенные частицы уплощаются.
# Каждое уплощение — предупреждение, потому что меняется смысл схемы.
def collect_particle(particle, elem_list, open_flag, type_name, depth):
if depth > 20:
return
for c in particle:
if not isinstance(c.tag, str) or etree.QName(c).namespace != XS_NS:
continue
ln = local(c)
if ln == "element":
elem_list.append(build_property(c, False))
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)
elif ln == "choice":
warn(type_name + " : вложенная xs:choice уплощена в последовательность — выбор одного из вариантов не сохранён")
collect_particle(c, elem_list, open_flag, type_name, depth + 1)
elif ln == "all":
warn(type_name + " : xs:all трактуется как последовательность")
collect_particle(c, elem_list, open_flag, type_name, depth + 1)
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)
else:
warn(type_name + " : не найдена группа " + str(c.get("ref")) + " — её свойства в пакет не попали")
if ln in ("sequence", "choice", "all", "group"):
if c.get("maxOccurs") is not None or c.get("minOccurs") is not None:
warn(type_name + " : кратность на вложенной частице (<xs:" + ln +
" minOccurs/maxOccurs>) не выражается в модели XDTO")
def set_type_flags(node, ct, is_open, choice):
m_open = MA(ct, "open")
if m_open is not None:
@@ -562,22 +624,40 @@ def fill_complex_type(node, ct):
seq = xfirst(body, "sequence")
cho = xfirst(body, "choice")
particle = seq if seq is not None else cho
is_open = False
all_ = xfirst(body, "all")
grp = xfirst(body, "group")
particle = seq if seq is not None else (cho if cho is not None else (all_ if all_ is not None else grp))
open_flag = [False]
# Порядок в XDTO: сначала form="Attribute", потом остальные (96.5% типов корпуса)
elem_props = []
type_name = ct.get("name") or "(анонимный тип)"
if particle is not None:
for c in particle:
if not isinstance(c.tag, str) or etree.QName(c).namespace != XS_NS:
continue
ln = local(c)
if ln == "element":
elem_props.append(build_property(c, False))
elif ln == "any":
is_open = True
if all_ is not None:
warn(type_name + " : xs:all трактуется как последовательность")
if grp is not None and seq is None and cho is None and all_ is None:
# Корневая частица задана ссылкой на группу — раскрываем её содержимое
g = resolve_group(grp, "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_props, open_flag, type_name, 1)
else:
warn(type_name + " : не найдена группа " + str(grp.get("ref")) + " — её свойства в пакет не попали")
else:
collect_particle(particle, elem_props, open_flag, type_name, 0)
is_open = open_flag[0]
for a in xchildren(body, "attribute"):
node.children.append(build_property(a, True))
# xs:attributeGroup раскрываем по ссылке
for ag in xchildren(body, "attributeGroup"):
g = resolve_group(ag, "attributeGroup")
if g is not None:
for a in xchildren(g, "attribute"):
node.children.append(build_property(a, True))
else:
warn("Не найдена группа атрибутов " + str(ag.get("ref")) + " — её атрибуты в пакет не попали")
node.children.extend(elem_props)
if xchildren(body, "anyAttribute"):
is_open = True
@@ -627,6 +707,30 @@ if ann is not None:
elif ln == "synonym":
meta_synonym.append({"Lang": f.get("lang") or "", "Content": f.text or ""})
# Реестр глобальных групп — нужен до обхода, чтобы раскрывать ссылки
for node in schema:
if not isinstance(node.tag, str) or etree.QName(node).namespace != XS_NS:
continue
nm = node.get("name")
if local(node) == "group" and nm:
GROUPS[nm] = node
if local(node) == "attributeGroup" and nm:
ATTR_GROUPS[nm] = node
# Конструкции XSD, которым в модели XDTO нет соответствия
for sg in schema.iter():
if isinstance(sg.tag, str) and local(sg) == "element" and sg.get("substitutionGroup"):
warn("Подстановочные группы (substitutionGroup) не поддерживаются моделью XDTO — объявление "
+ str(sg.get("name")) + " сохранено как обычное")
for idc in ("key", "keyref", "unique"):
if any(isinstance(e.tag, str) and local(e) == idc for e in schema.iter()):
warn("Ограничения целостности (xs:" + idc + ") в модели XDTO не хранятся — отброшены")
if any(isinstance(e.tag, str) and local(e) == "redefine" for e in schema.iter()):
warn("xs:redefine не поддерживается — переопределения проигнорированы")
if any(isinstance(e.tag, str) and local(e) == "include" for e in schema.iter()):
warn("xs:include проигнорирован: модель XDTO разрешает зависимости только по namespace. "
"Соберите включаемую схему отдельным пакетом и добавьте <xs:import>")
for node in schema:
if not isinstance(node.tag, str) or etree.QName(node).namespace != XS_NS:
continue
@@ -635,7 +739,7 @@ for node in schema:
n = Node("import")
add_attr(n, "namespace", node.get("namespace"))
pkg_node.children.append(n)
elif ln in ("annotation", "include"):
elif ln in ("annotation", "include", "group", "attributeGroup", "notation"):
continue
elif ln == "element":
pkg_node.children.append(build_property(node, False))
@@ -772,6 +876,13 @@ print(f"✓ Пакет XDTO собран: {name}")
print(f" Namespace: {target_ns}")
print(f" Типов: {type_count}")
print(f" Файлы: XDTOPackages/{name}.xml, XDTOPackages/{name}/Ext/Package.bin")
if warnings_list:
print("")
print("Предупреждения (" + str(len(warnings_list)) +
") — конструкции XSD без точного соответствия в модели XDTO:")
for w in warnings_list:
print(" ! " + w)
print("")
if reg_result == "added":
print(f" Configuration.xml: <XDTOPackage>{name}</XDTOPackage> добавлен в ChildObjects")
elif reg_result == "already":