From 7ca6dfa6b2f52dc623dbb1f078025e75f2c8699e Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 25 Jul 2026 21:48:48 +0300 Subject: [PATCH] =?UTF-8?q?fix(xdto-compile,xdto-validate):=20=D0=BD=D0=B5?= =?UTF-8?q?=20=D1=82=D0=B5=D1=80=D1=8F=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=86=D0=B8=D0=B8=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=BB=D1=87=D0=B0;=20=D0=B2=D1=8B=D1=87=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B8=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .claude/skills/xdto-compile/SKILL.md | 84 +++--- .../xdto-compile/scripts/xdto-compile.ps1 | 137 +++++++++- .../xdto-compile/scripts/xdto-compile.py | 133 ++++++++- .claude/skills/xdto-compile/xsd-reference.md | 88 ++++++ .claude/skills/xdto-decompile/SKILL.md | 48 ++-- .claude/skills/xdto-validate/SKILL.md | 42 +-- .../xdto-validate/scripts/xdto-validate.ps1 | 137 +++++++++- .../xdto-validate/scripts/xdto-validate.py | 135 +++++++++- docs/xdto-dsl-spec.md | 24 +- .../cases/xdto-compile/nested-particles.json | 14 + .../cases/xdto-compile/nested-particles.xsd | 28 ++ .../nested-particles/Configuration.xml | 252 ++++++++++++++++++ .../Ext/ClientApplicationInterface.xml | 18 ++ .../nested-particles/Languages/Русский.xml | 16 ++ .../XDTOPackages/nested_particles.xml | 16 ++ .../nested_particles/Ext/Package.bin | 16 ++ .../nested-particles/nested-particles.xsd | 28 ++ .../pkg-badorder/XDTOPackages/badorder.xml | 11 + .../XDTOPackages/badorder/Ext/Package.bin | 6 + .../pkg-conflict/XDTOPackages/conflict.xml | 11 + .../XDTOPackages/conflict/Ext/Package.bin | 11 + .../cases/xdto-validate/silent-anytype.json | 2 +- .../xdto-validate/structural-conflicts.json | 18 ++ .../cases/xdto-validate/top-level-order.json | 13 + 24 files changed, 1156 insertions(+), 132 deletions(-) create mode 100644 .claude/skills/xdto-compile/xsd-reference.md create mode 100644 tests/skills/cases/xdto-compile/nested-particles.json create mode 100644 tests/skills/cases/xdto-compile/nested-particles.xsd create mode 100644 tests/skills/cases/xdto-compile/snapshots/nested-particles/Configuration.xml create mode 100644 tests/skills/cases/xdto-compile/snapshots/nested-particles/Ext/ClientApplicationInterface.xml create mode 100644 tests/skills/cases/xdto-compile/snapshots/nested-particles/Languages/Русский.xml create mode 100644 tests/skills/cases/xdto-compile/snapshots/nested-particles/XDTOPackages/nested_particles.xml create mode 100644 tests/skills/cases/xdto-compile/snapshots/nested-particles/XDTOPackages/nested_particles/Ext/Package.bin create mode 100644 tests/skills/cases/xdto-compile/snapshots/nested-particles/nested-particles.xsd create mode 100644 tests/skills/cases/xdto-validate/pkg-badorder/XDTOPackages/badorder.xml create mode 100644 tests/skills/cases/xdto-validate/pkg-badorder/XDTOPackages/badorder/Ext/Package.bin create mode 100644 tests/skills/cases/xdto-validate/pkg-conflict/XDTOPackages/conflict.xml create mode 100644 tests/skills/cases/xdto-validate/pkg-conflict/XDTOPackages/conflict/Ext/Package.bin create mode 100644 tests/skills/cases/xdto-validate/structural-conflicts.json create mode 100644 tests/skills/cases/xdto-validate/top-level-order.json diff --git a/.claude/skills/xdto-compile/SKILL.md b/.claude/skills/xdto-compile/SKILL.md index 2811f1d8..8bed895d 100644 --- a/.claude/skills/xdto-compile/SKILL.md +++ b/.claude/skills/xdto-compile/SKILL.md @@ -1,7 +1,7 @@ --- name: xdto-compile description: Создание пакета XDTO 1С из XML-схемы (XSD). Используй когда нужно добавить пакет XDTO в конфигурацию — для обмена, интеграции, веб-сервиса, разбора внешнего XML-формата -argument-hint: -XsdPath <файл.xsd> -OutputDir <каталог-конфигурации> [-Name <имя>] [-Synonym <синоним>] [-Comment <текст>] [-Force] +argument-hint: -XsdPath <файл.xsd> -OutputDir <каталог-исходников> [-Name <имя>] [-Synonym <синоним>] [-Comment <текст>] [-Force] allowed-tools: - Bash - Read @@ -10,12 +10,11 @@ allowed-tools: # /xdto-compile — Создание пакета XDTO из XML-схемы -Собирает пакет XDTO по обычной XML-схеме: `XDTOPackages/<Имя>.xml`, +Собирает пакет XDTO по XML-схеме: `XDTOPackages/<Имя>.xml`, `XDTOPackages/<Имя>/Ext/Package.bin` и регистрацию в `Configuration.xml`. -Отдельного DSL нет — входной формат это XSD. Вся механика перевода (локальные объявления -префиксов `dNpM` на каждой ссылке, инвертированная кратность `lowerBound`/`upperBound`, -фасеты атрибутами вместо дочерних элементов) делается навыком. +Вход — обычная XSD. Отдельного формата описания нет: пиши схему так, как её пишут +везде, остальное навык сделает сам. ## Параметры @@ -23,34 +22,65 @@ allowed-tools: |----------|----------| | `XsdPath` | Путь к файлу XML-схемы | | `Xsd` | Схема строкой, вместо `-XsdPath` | -| `OutputDir` | Каталог конфигурации (где лежит `Configuration.xml`) | -| `Name` | Имя объекта метаданных. По умолчанию — из `xs:appinfo`, иначе имя файла XSD, санированное под идентификатор 1С | -| `Synonym` | Синоним: строка (русский) или хеш-таблица `@{ru='…'; en='…'}` | +| `OutputDir` | Каталог исходников конфигурации или расширения — там, где лежит `Configuration.xml` | +| `Name` | Имя объекта метаданных. По умолчанию — из `xs:appinfo`, иначе имя файла XSD | +| `Synonym` | Синоним: строка или хеш-таблица `@{ru='…'; en='…'}` | | `Comment` | Комментарий | | `Force` | Перезаписать существующий пакет | ```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/xdto-compile.ps1" -XsdPath "<схема.xsd>" -OutputDir "<каталог>" +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/xdto-compile.ps1" -XsdPath "<схема.xsd>" -OutputDir "<каталог-исходников>" ``` Примеры: ```powershell -... -XsdPath bank.xsd -OutputDir C:\cfsrc\erp -Name ОбменСБанком -Synonym "Обмен с банком" -... -XsdPath fss.xsd -OutputDir C:\cfsrc\erp -Force +... -XsdPath bank.xsd -OutputDir src -Name ОбменСБанком -Synonym "Обмен с банком" +... -XsdPath fss.xsd -OutputDir src -Force ``` -## Формат входа +## Читай предупреждения -Обычная XML-схема. Две вещи, которые XSD выразить не может (`nillable` у атрибута, -`qualified` у отдельного свойства), пишутся атрибутами из пространства имён модели XDTO: +XSD выразительнее модели XDTO. Всё, что не переносится один в один, навык переносит +приближённо и **пишет об этом**: + +``` +Предупреждения (2) — конструкции XSD без точного соответствия в модели XDTO: + ! Документ : вложенная xs:choice уплощена в последовательность — выбор одного из вариантов не сохранён + ! Документ : кратность на вложенной частице () не выражается в модели XDTO +``` + +Такое сообщение означает, что пакет собран, но схема упрощена. Если упрощение +недопустимо — меняй схему (например, разноси варианты `xs:choice` по разным типам), +а не игнорируй. + +Что переносится приближённо: вложенные `xs:sequence`/`xs:choice` (уплощаются в плоский +список свойств), `xs:all` (становится последовательностью), кратность на частице, +`substitutionGroup`, `xs:key`/`keyref`/`unique`, `xs:redefine`. + +`xs:group` и `xs:attributeGroup` раскрываются по ссылке — их содержимое попадает в тип. +`xs:include` игнорируется: зависимости в XDTO разрешаются только по namespace, +поэтому включаемую схему нужно собрать отдельным пакетом и заменить `include` на `import`. + +## Зависимости между пакетами + +`` разрешается по namespace среди пакетов конфигурации +или расширения. +Если пакета с таким пространством имён нет, платформа при загрузке молча подменит тип +на `xs:anyType` — без ошибки. Собирай сначала зависимости, потом зависящий пакет, +и проверяй результат через `/xdto-validate`. + +## Что XSD выразить не может + +Две вещи модель XDTO умеет, а XML Schema — нет: `nillable` у атрибута и `qualified` +у отдельного свойства. Они пишутся атрибутами из пространства имён модели: ```xml ``` -Правило: **чего XSD сказать не может — пиши атрибутом `xdto:` с тем же именем, что в модели**. -Схема при этом остаётся валидной. Полный список аннотаций — в справке ниже. +Схема остаётся валидной — валидаторы такие атрибуты игнорируют. Полный список +и таблица соответствий XSD ↔ XDTO — в [xsd-reference.md](xsd-reference.md). Свойства объекта метаданных можно задать прямо в схеме: @@ -65,29 +95,17 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/xdto-compile.ps1" - ``` -Принимается и не вполне каноническая схема: имя типа без префикса трактуется как тип -целевого пространства имён, `form="qualified"` на глобальных объявлениях (так экспортирует -сам Конфигуратор) — читается. - -## Зависимости между пакетами - -`` разрешается по namespace среди пакетов конфигурации. -Если пакета с таким пространством имён в конфигурации нет, платформа при загрузке молча -подменит тип на `xs:anyType` — без ошибки. Сначала соберите пакеты-зависимости, -затем зависящий, и проверьте результат через `/xdto-validate`. - ## Типичный workflow -1. Получить XSD от контрагента (или выгрузить: Конфигуратор → пакет → Экспорт XML-схемы) -2. `/xdto-compile -XsdPath <файл> -OutputDir <конфигурация>` -3. `/xdto-validate` — убедиться, что все типы разрешились -4. `/db-load-xml` + `/db-update` — загрузить в базу +1. Получить XSD от контрагента (или выгрузить схему существующего пакета: `/xdto-decompile`) +2. `/xdto-compile -XsdPath <файл> -OutputDir <каталог-исходников>` — прочитать предупреждения +3. `/xdto-validate` — убедиться, что типы разрешились +4. `/db-load-xml` + `/db-update` Правка существующего пакета: `/xdto-decompile` → правка XSD → `/xdto-compile -Force`. -Пара замыкается без потерь, включая свойства объекта метаданных. ## Верификация ``` -/xdto-validate <каталог>/XDTOPackages/<Имя> — типы, импорты, регистрация +/xdto-validate <каталог-исходников>/XDTOPackages/<Имя> ``` diff --git a/.claude/skills/xdto-compile/scripts/xdto-compile.ps1 b/.claude/skills/xdto-compile/scripts/xdto-compile.ps1 index 6ed312e4..eb6ec60b 100644 --- a/.claude/skills/xdto-compile/scripts/xdto-compile.ps1 +++ b/.claude/skills/xdto-compile/scripts/xdto-compile.ps1 @@ -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 : кратность на вложенной частице () не выражается в модели 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. Соберите включаемую схему отдельным пакетом и добавьте " +} + 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: $Name добавлен в ChildObjects" } "already" { Write-Host " Configuration.xml: $Name уже зарегистрирован" } diff --git a/.claude/skills/xdto-compile/scripts/xdto-compile.py b/.claude/skills/xdto-compile/scripts/xdto-compile.py index c411ebbb..5d7e420d 100644 --- a/.claude/skills/xdto-compile/scripts/xdto-compile.py +++ b/.claude/skills/xdto-compile/scripts/xdto-compile.py @@ -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 + " : кратность на вложенной частице () не выражается в модели 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. " + "Соберите включаемую схему отдельным пакетом и добавьте ") + 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: {name} добавлен в ChildObjects") elif reg_result == "already": diff --git a/.claude/skills/xdto-compile/xsd-reference.md b/.claude/skills/xdto-compile/xsd-reference.md new file mode 100644 index 00000000..daa577b0 --- /dev/null +++ b/.claude/skills/xdto-compile/xsd-reference.md @@ -0,0 +1,88 @@ +# XSD ↔ XDTO — справочник + +## Соответствия + +| XML Schema | Модель XDTO | +|---|---| +| `xs:schema/@targetNamespace` | пространство имён пакета | +| `elementFormDefault` / `attributeFormDefault` | `elementFormQualified` / `attributeFormQualified` | +| `xs:import/@namespace` | зависимость от другого пакета (разрешается по namespace) | +| `xs:complexType` | объектный тип | +| `xs:simpleType` | тип значения | +| `xs:element` / `xs:attribute` на верхнем уровне | глобальное свойство пакета | +| `xs:element` / `xs:attribute` внутри типа | свойство типа | +| `@minOccurs` / `@maxOccurs="unbounded"` | `lowerBound` / `upperBound="-1"` | +| `@nillable`, `@default`, `@fixed`, `@ref` | те же по смыслу | +| анонимный `xs:simpleType`/`xs:complexType` в объявлении | встроенный тип свойства | +| `xs:complexContent/xs:extension/@base` | наследование типа | +| `@abstract`, `@mixed` | те же | +| `xs:choice` | тип-выбор одного из вариантов | +| `xs:any` + `xs:anyAttribute` | открытый тип | +| `xs:simpleContent/xs:extension/@base` | свойство собственного значения элемента | +| `xs:restriction` + фасеты | базовый тип + ограничения | +| `xs:pattern`, `xs:enumeration` | те же | +| `xs:list/@itemType` | список | +| `xs:union/@memberTypes` | объединение | + +Порядок объявлений верхнего уровня в XSD произвольный — навык сам расставит их +в порядке, который требует модель. + +## Аннотации `xdto:` + +Пространство имён — `http://v8.1c.ru/8.1/xdto`. Нужны только там, где XML Schema +не может выразить то, что умеет модель. В большинстве схем не нужны вовсе. + +Правило: **чего XSD сказать не может — пиши атрибутом `xdto:` с тем же именем, +что и в модели**. + +| Аннотация | Где | Назначение | +|---|---|---| +| `xdto:nillable` | `xs:attribute` | `nillable` у свойства-атрибута (XSD допускает только у элементов) | +| `xdto:lowerBound`, `xdto:upperBound` | `xs:attribute` | кратность свойства-атрибута | +| `xdto:qualified` | объявление | переопределение `*FormQualified` для одного свойства | +| `xdto:name` | объявление | имя свойства, если XML-имя не годится как идентификатор 1С; XML-имя уйдёт в `localName` | +| `xdto:form` | `xs:element` | записать `form` явно | +| `xdto:variety` | `xs:restriction`, `xs:list`, `xs:union` | записать разновидность типа явно | +| `xdto:open`, `xdto:abstract`, `xdto:mixed`, `xdto:ordered`, `xdto:sequenced` | `xs:complexType` | флаги типа, не выводимые из модели содержимого | +| `xdto:order` | `xs:complexType` | исходный порядок свойств, если он не «атрибуты первыми»; имена через `\|` | +| `xdto:textName`, `xdto:textlowerBound`, `xdto:textupperBound`, `xdto:textnillable` | `xs:extension` в `xs:simpleContent` | параметры свойства собственного значения | +| `xdto:type` | `xs:enumeration` | тип литерала перечисления | +| `xdto:prefix` | объявление | осмысленный префикс пространства имён вместо генерируемого | +| `xdto:memberTypesForm="prefixed"` | `xs:union` | записать состав объединения префиксами, а не `{ns}имя` | +| `xdto:declareNs` | `xs:union` | объявить префикс пространства имён на узле | +| `xdto:elementFormQualified`, `xdto:attributeFormQualified` | `xs:schema` | записать флаги явно | + +Пример: + +```xml + + + + + + + +``` + +Аннотации, которые проставляет `/xdto-decompile` при выгрузке существующего пакета, +писать вручную не нужно — они нужны, чтобы обратная сборка вернула ровно тот же файл. + +## Свойства объекта метаданных + +```xml + + + + ОбменСБанком + Обмен с банком + Bank exchange + Формат 1С:Предприятие — Клиент банка + + + +``` + +Пространство имён пакета берётся из `targetNamespace` и здесь не дублируется. +Параметры `-Name`, `-Synonym`, `-Comment` имеют приоритет над этим блоком. diff --git a/.claude/skills/xdto-decompile/SKILL.md b/.claude/skills/xdto-decompile/SKILL.md index 105d8a14..4ed44ec6 100644 --- a/.claude/skills/xdto-decompile/SKILL.md +++ b/.claude/skills/xdto-decompile/SKILL.md @@ -10,16 +10,15 @@ allowed-tools: # /xdto-decompile — Выгрузка пакета XDTO в XML-схему -Превращает `Ext/Package.bin` в обычную XML-схему. Заменяет чтение модели XDTO с её -инвертированной кратностью, фасетами-атрибутами и локальными объявлениями префиксов -`dNpM` на каждой ссылке. +Превращает пакет XDTO в обычную XML-схему — читаемую и редактируемую. +Заменяет чтение `Ext/Package.bin` напрямую. ## Параметры | Параметр | Описание | |----------|----------| | `PackagePath` | Каталог пакета, путь к `Ext/Package.bin` или к `<Имя>.xml` объекта метаданных | -| `OutFile` | Записать схему в файл (UTF-8 BOM). Без него — вывод в stdout | +| `OutFile` | Записать схему в файл. Без него — вывод в stdout | ```powershell powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/xdto-decompile.ps1" -PackagePath "<путь>" @@ -27,38 +26,27 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/xdto-decompile.ps1" Примеры: ```powershell -... -PackagePath C:\cfsrc\erp\XDTOPackages\ClientBankExchange -... -PackagePath C:\cfsrc\erp\XDTOPackages\ClientBankExchange -OutFile bank.xsd +... -PackagePath src/XDTOPackages/ОбменСБанком +... -PackagePath src/XDTOPackages/ОбменСБанком -OutFile bank.xsd ``` -## Round-trip +## Правка пакета -`/xdto-decompile` → правка XSD → `/xdto-compile -Force` возвращает исходный `Package.bin` -байт-в-байт. Инвариант проверен на 760 пакетах выгрузок Бухгалтерии и ERP. +`/xdto-decompile` → правка XSD → `/xdto-compile -Force` возвращает пакет без потерь, +включая имя, синоним и комментарий объекта метаданных — они выгружаются +в `xs:annotation/xs:appinfo`. Это основной способ менять существующий пакет: +отдельных операций правки не нужно, схема читается и правится целиком. -Это основной способ править существующий пакет: точечных операций не требуется, схема -целиком читаема и редактируема. +В схеме могут встретиться атрибуты с префиксом `xdto:` — так записано то, что +XML Schema выразить не может (например `nillable` у атрибута). Схема при этом остаётся +валидной, валидаторы такие атрибуты игнорируют. Трогать их обычно не нужно; смысл +каждого описан в справочнике навыка `/xdto-compile`. -Свойства объекта метаданных (`Name`, `Synonym`, `Comment`) выгружаются в -`xs:annotation/xs:appinfo`, поэтому при обратной сборке не теряются. `Namespace` не -дублируется — его несёт `targetNamespace`. +## Передача схемы наружу -## Отличия от экспорта XML-схемы в Конфигураторе - -Штатный экспорт Конфигуратора теряет данные: `nillable` у свойств-атрибутов -(спецификация XSD не допускает его у атрибутов), а для пакетов с -`elementFormQualified="false"` выдаёт невалидную схему — `form="qualified"` на -глобальных объявлениях. - -Навык пишет валидную схему, а то, что XSD выразить не может, выносит в атрибуты -пространства имён модели XDTO: - -```xml - -``` - -Такие атрибуты валидаторы игнорируют, а `/xdto-compile` читает обратно. Схему без -потерь можно отдавать контрагенту как есть. +Полученную XSD можно отдавать контрагенту как есть. В отличие от команды +«Экспорт XML-схемы» в Конфигураторе, навык не теряет `nillable` у свойств-атрибутов +и не пишет `form` на глобальных объявлениях, где спецификация XSD его не допускает. ## Верификация diff --git a/.claude/skills/xdto-validate/SKILL.md b/.claude/skills/xdto-validate/SKILL.md index 354b0989..ac56c80c 100644 --- a/.claude/skills/xdto-validate/SKILL.md +++ b/.claude/skills/xdto-validate/SKILL.md @@ -11,15 +11,15 @@ allowed-tools: # /xdto-validate — Валидация пакета XDTO Проверяет модель пакета, объект метаданных и его связь с конфигурацией. -Exit code `1` при наличии ошибок. +Каждая находка выводится отдельной строкой с объяснением. Exit code `1` при ошибках. ## Параметры | Параметр | Описание | |----------|----------| | `PackagePath` | Каталог пакета, `Ext/Package.bin` или `<Имя>.xml` объекта метаданных | -| `ConfigDir` | Каталог конфигурации. По умолчанию определяется по расположению пакета | -| `Detailed` | Показывать успешные проверки, а не только ошибки | +| `ConfigDir` | Корень исходников. По умолчанию определяется по расположению пакета | +| `Detailed` | Показывать успешные проверки, а не только проблемы | | `MaxErrors` | Остановиться после N ошибок (по умолчанию 20) | | `OutFile` | Записать отчёт в файл | @@ -27,39 +27,19 @@ Exit code `1` при наличии ошибок. powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/xdto-validate.ps1" -PackagePath "<путь>" ``` -## Проверки +`[ERROR]` — платформа такой пакет не примет либо примет неправильно. +`[WARN]` — пакет рабочий, но есть риск, о котором стоит знать. -**Модель** -- Package.bin — корректный XML с корнем ``, задан `targetNamespace`, кодировка UTF-8 с BOM -- Имена типов уникальны внутри пакета -- Каждая ссылка (`type`, `base`, `ref`, `itemType`, `memberTypes`) разрешается: собственный - тип, импортированное пространство имён или `xs:`. Поддержана нотация Кларка `{ns}local` -- Префиксы объявлены (нет ссылок на необъявленный `dNpM`) -- Фасеты согласованы: `length` не вместе с `minLength`/`maxLength`, `minLength ≤ maxLength`, - `fractionDigits ≤ totalDigits`, допустимые `whiteSpace` и `variety` -- Свойства: допустимая `form`, `lowerBound ≤ upperBound`, есть `name` либо `ref` +## Зачем запускать, если пакет и так грузится -**Связь с конфигурацией** -- Объект метаданных существует, `Name` совпадает с именем каталога, `Namespace` — с `targetNamespace` -- Пакет зарегистрирован в `ChildObjects` файла `Configuration.xml` -- `targetNamespace` уникален среди пакетов конфигурации - -**Предупреждения о тихих дефектах** - -Два класса проблем платформа не диагностирует — их ловит навык: - -- **`xs:anyType` при объявленных импортах.** При импорте XML-схемы тип из чужого - пространства имён, для которого в конфигурации нет пакета, заменяется на `xs:anyType` - молча — ни ошибки, ни предупреждения. Пакет выглядит загруженным, а `ФабрикаXDTO` - потом отдаёт бесструктурное значение. Сопутствующий признак — объявленный, но - неиспользуемый ``. -- **`nillable="true"` вместе с `form="Attribute"`.** Спецификация XSD не допускает - `nillable` у атрибутов, поэтому экспорт XML-схемы в Конфигураторе такое свойство теряет. - На саму работу пакета не влияет, но ломает перенос схемы через Конфигуратор. +Часть дефектов платформа не диагностирует: неразрешённый тип из чужого пространства +имён она молча подменяет на `xs:anyType`, и пакет выглядит загруженным, пока +`ФабрикаXDTO` не отдаст в рантайме бесструктурное значение. Такие вещи видно только +статически — до загрузки в базу. ## Типичный workflow -1. `/xdto-compile` или `/xdto-decompile` → правка → `/xdto-compile -Force` +1. `/xdto-compile` или правка через `/xdto-decompile` → `/xdto-compile -Force` 2. `/xdto-validate <путь>` — до загрузки в базу 3. `/db-load-xml` + `/db-update` diff --git a/.claude/skills/xdto-validate/scripts/xdto-validate.ps1 b/.claude/skills/xdto-validate/scripts/xdto-validate.ps1 index 525bdeb7..434dc452 100644 --- a/.claude/skills/xdto-validate/scripts/xdto-validate.ps1 +++ b/.claude/skills/xdto-validate/scripts/xdto-validate.ps1 @@ -140,18 +140,40 @@ if (-not ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -a $imports = New-Object System.Collections.ArrayList $localTypes = New-Object System.Collections.Generic.HashSet[string] +$objectTypeNames = New-Object System.Collections.Generic.HashSet[string] +$valueTypeNames = New-Object System.Collections.Generic.HashSet[string] $globalProps = New-Object System.Collections.Generic.HashSet[string] +$topSequence = New-Object System.Collections.ArrayList foreach ($n in $pkg.ChildNodes) { if ($n.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } + [void]$topSequence.Add($n.get_LocalName()) switch ($n.get_LocalName()) { "import" { [void]$imports.Add($n.GetAttribute("namespace")) } - "objectType" { [void]$localTypes.Add($n.GetAttribute("name")) } - "valueType" { [void]$localTypes.Add($n.GetAttribute("name")) } + "objectType" { [void]$localTypes.Add($n.GetAttribute("name")); [void]$objectTypeNames.Add($n.GetAttribute("name")) } + "valueType" { [void]$localTypes.Add($n.GetAttribute("name")); [void]$valueTypeNames.Add($n.GetAttribute("name")) } "property" { if ($n.HasAttribute("name")) { [void]$globalProps.Add($n.GetAttribute("name")) } } } } +# --- Порядок элементов верхнего уровня --- +# Модель требует import -> property -> valueType -> objectType. Нарушение платформа +# не прощает: db-update падает с «Ошибка преобразования данных XDTO». +$TOP_ORDER = @("import", "property", "valueType", "objectType") +$prevRank = -1 +$orderOk = $true +foreach ($t in $topSequence) { + $rank = [array]::IndexOf($TOP_ORDER, $t) + if ($rank -lt 0) { continue } + if ($rank -lt $prevRank) { + Report-Error "Нарушен порядок элементов верхнего уровня: <$t> после <$($TOP_ORDER[$prevRank])>. Модель требует import -> property -> valueType -> objectType; платформа отвергнет пакет при обновлении конфигурации" + $orderOk = $false + break + } + $prevRank = $rank +} +if ($orderOk) { Report-OK "Порядок элементов верхнего уровня корректен" } + # --- 4. Duplicate type names --- $seen = @{} @@ -249,12 +271,15 @@ if ($anyTypeProps.Count -gt 0 -and $imports.Count -gt 0) { # --- 7. Unused imports --- -foreach ($imp in $imports) { - if (-not $usedNamespaces.Contains($imp)) { - Report-Warn " объявлен, но ни один тип из этого пространства имён не используется" - } +# Сам по себе неиспользуемый импорт безвреден и встречается в четверти пакетов +# типовых конфигураций. Сигналом он становится только вместе с anyType — тогда это +# почти наверняка неразрешённая зависимость. +$unused = @() +foreach ($imp in $imports) { if (-not $usedNamespaces.Contains($imp)) { $unused += $imp } } +if ($unused.Count -gt 0 -and $anyTypeProps.Count -gt 0) { + Report-Warn "Импорт(ы) без единого использованного типа: $($unused -join ', ') — вместе с anyType это признак неразрешённой зависимости" } -if ($imports.Count -gt 0 -and $script:warnings -eq 0) { Report-OK "$($imports.Count) импорт(ов) — все используются" } +if ($imports.Count -gt 0 -and $unused.Count -eq 0) { Report-OK "$($imports.Count) импорт(ов) — все используются" } # --- 8. nillable on attribute-form properties --- @@ -279,7 +304,8 @@ foreach ($t in $pkg.SelectNodes("//*[local-name()='valueType' or local-name()='t $len = $t.GetAttribute("length") if ($len -and ($t.HasAttribute("minLength") -or $t.HasAttribute("maxLength"))) { - Report-Error "$nm : length несовместим с minLength/maxLength" + # Спецификация XSD это запрещает, но платформа такие типы хранит — предупреждение, не ошибка + Report-Warn "$nm : length задан вместе с minLength/maxLength — спецификация XSD считает их взаимоисключающими" } $minL = $t.GetAttribute("minLength"); $maxL = $t.GetAttribute("maxLength") if ($minL -and $maxL -and ([int]$minL -gt [int]$maxL)) { @@ -328,6 +354,99 @@ foreach ($p in $pkg.SelectNodes("//*[local-name()='property']")) { } if (-not $script:stopped) { Report-OK "Свойства: form и кратности корректны" } +# --- 10b. Structural consistency --- + +$structOk = $true +foreach ($t in $pkg.SelectNodes("//*[local-name()='objectType' or local-name()='typeDef']")) { + if ($t.get_LocalName() -eq "typeDef" -and $t.GetAttribute("type", $XSI_NS) -ne "ObjectType") { continue } + $tn = if ($t.HasAttribute("name")) { $t.GetAttribute("name") } else { "(анонимный тип)" } + $propNames = @{} + foreach ($c in $t.ChildNodes) { + if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element -or $c.get_LocalName() -ne "property") { continue } + $pn = $c.GetAttribute("name") + if ($pn) { + if ($propNames.ContainsKey($pn)) { + Report-Error "$tn : дублирующееся имя свойства `"$pn`"" + $structOk = $false + } + $propNames[$pn] = $true + } + } + if ($script:stopped) { break } +} + +foreach ($p in $pkg.SelectNodes("//*[local-name()='property']")) { + $pn = if ($p.HasAttribute("name")) { $p.GetAttribute("name") } else { $p.GetAttribute("ref") } + if ($p.HasAttribute("name") -and $p.HasAttribute("ref")) { + Report-Error "Свойство `"$pn`": заданы одновременно name и ref — допустимо только одно" + $structOk = $false + } + $inlineTypeDef = $null + foreach ($c in $p.ChildNodes) { + if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "typeDef") { $inlineTypeDef = $c; break } + } + if ($inlineTypeDef -and $p.HasAttribute("type")) { + Report-Error "Свойство `"$pn`": заданы одновременно type и вложенный — допустимо только одно" + $structOk = $false + } + if ($inlineTypeDef -and -not $inlineTypeDef.HasAttribute("type", $XSI_NS)) { + Report-Error "Свойство `"$pn`": у вложенного не задан xsi:type (ValueType или ObjectType)" + $structOk = $false + } + if ($script:stopped) { break } +} + +# Анонимный тип внутри valueType задаёт базовый тип и xsi:type не несёт +foreach ($vt in $pkg.SelectNodes("//*[local-name()='valueType']")) { + foreach ($c in $vt.ChildNodes) { + if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element -or $c.get_LocalName() -ne "typeDef") { continue } + if ($c.HasAttribute("type", $XSI_NS)) { + Report-Warn "$($vt.GetAttribute('name')) : у внутри задан xsi:type — платформа его здесь не пишет" + } + } +} + +# Род базового типа должен совпадать +foreach ($t in $pkg.SelectNodes("//*[local-name()='objectType'][@base]")) { + $b = $t.GetAttribute("base") + $parts = $b.Split(":") + if ($parts.Count -ne 2) { continue } + $bns = $t.GetNamespaceOfPrefix($parts[0]) + if ($bns -ne $targetNs) { continue } + if ($valueTypeNames.Contains($parts[1])) { + Report-Error "$($t.GetAttribute('name')) : base=`"$b`" ссылается на valueType, а objectType может наследоваться только от objectType" + $structOk = $false + } +} +foreach ($t in $pkg.SelectNodes("//*[local-name()='valueType'][@base]")) { + $b = $t.GetAttribute("base") + $parts = $b.Split(":") + if ($parts.Count -ne 2) { continue } + $bns = $t.GetNamespaceOfPrefix($parts[0]) + if ($bns -ne $targetNs) { continue } + if ($objectTypeNames.Contains($parts[1])) { + Report-Error "$($t.GetAttribute('name')) : base=`"$b`" ссылается на objectType, а valueType может строиться только на простом типе" + $structOk = $false + } +} + +# Union без состава +foreach ($t in $pkg.SelectNodes("//*[local-name()='valueType' or local-name()='typeDef'][@variety='Union']")) { + if ($t.HasAttribute("memberTypes")) { continue } + $hasMember = $false + foreach ($c in $t.ChildNodes) { + if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "typeDef") { $hasMember = $true; break } + } + if (-not $hasMember) { + $tn = if ($t.HasAttribute("name")) { $t.GetAttribute("name") } else { "(анонимный тип)" } + Report-Warn "$tn : variety=`"Union`" без memberTypes и без вложенных типов — состав объединения пуст" + } +} + +if ($structOk -and -not $script:stopped) { Report-OK "Структура типов и свойств согласована" } + +if ($script:stopped) { & $finalize; exit 1 } + # --- 11. Metadata object --- if ($mdPath) { @@ -386,7 +505,7 @@ if (Test-Path $configXml) { } catch {} } if ($clash.Count -gt 0) { - Report-Error "targetNamespace `"$targetNs`" уже занят пакет(ами): $($clash -join ', '). Платформа не допускает два пакета с одним пространством имён" + Report-Warn "targetNamespace `"$targetNs`" объявлен также в пакет(ах): $($clash -join ', '). Платформа это допускает, но на это пространство имён становится неоднозначным" } else { Report-OK "targetNamespace уникален в конфигурации" } diff --git a/.claude/skills/xdto-validate/scripts/xdto-validate.py b/.claude/skills/xdto-validate/scripts/xdto-validate.py index 74030839..5a49e5d1 100644 --- a/.claude/skills/xdto-validate/scripts/xdto-validate.py +++ b/.claude/skills/xdto-validate/scripts/xdto-validate.py @@ -145,19 +145,47 @@ else: imports = [] local_types = set() +object_type_names = set() +value_type_names = set() global_props = set() +top_sequence = [] for n in pkg: if not isinstance(n.tag, str): continue ln = local(n) + top_sequence.append(ln) if ln == "import": imports.append(n.get("namespace")) - elif ln in ("objectType", "valueType"): + elif ln == "objectType": local_types.add(n.get("name")) + object_type_names.add(n.get("name")) + elif ln == "valueType": + local_types.add(n.get("name")) + value_type_names.add(n.get("name")) elif ln == "property" and n.get("name"): global_props.add(n.get("name")) +# ── порядок элементов верхнего уровня ──────────────────────── +# Модель требует import -> property -> valueType -> objectType. Нарушение платформа +# не прощает: db-update падает с «Ошибка преобразования данных XDTO». +TOP_ORDER = ["import", "property", "valueType", "objectType"] +prev_rank = -1 +order_ok = True +for t in top_sequence: + if t not in TOP_ORDER: + continue + rank = TOP_ORDER.index(t) + if rank < prev_rank: + report_error(f"Нарушен порядок элементов верхнего уровня: <{t}> после <{TOP_ORDER[prev_rank]}>. " + "Модель требует import -> property -> valueType -> objectType; " + "платформа отвергнет пакет при обновлении конфигурации") + order_ok = False + break + prev_rank = rank +if order_ok: + report_ok("Порядок элементов верхнего уровня корректен") + # ── 4. duplicate type names ────────────────────────────────── seen = set() @@ -254,10 +282,13 @@ if any_type_props and imports: # ── 7. unused imports ──────────────────────────────────────── -for imp in imports: - if imp not in used_namespaces: - report_warn(f' объявлен, но ни один тип из этого пространства имён не используется') -if imports and state["warnings"] == 0: +# Сам по себе неиспользуемый импорт безвреден и встречается в четверти пакетов +# типовых конфигураций. Сигналом он становится только вместе с anyType. +unused = [i for i in imports if i not in used_namespaces] +if unused and any_type_props: + report_warn(f'Импорт(ы) без единого использованного типа: {", ".join(unused)} — ' + "вместе с anyType это признак неразрешённой зависимости") +if imports and not unused: report_ok(f"{len(imports)} импорт(ов) — все используются") # ── 8. nillable on attribute-form properties ───────────────── @@ -283,7 +314,9 @@ for t in pkg.iter(): nm = t.get("name") or "(анонимный тип)" if t.get("length") and (t.get("minLength") or t.get("maxLength")): - report_error(f"{nm} : length несовместим с minLength/maxLength") + # Спецификация XSD это запрещает, но платформа такие типы хранит — предупреждение, не ошибка + report_warn(f"{nm} : length задан вместе с minLength/maxLength — " + "спецификация XSD считает их взаимоисключающими") min_l, max_l = t.get("minLength"), t.get("maxLength") if min_l and max_l and int(min_l) > int(max_l): report_error(f"{nm} : minLength ({min_l}) больше maxLength ({max_l})") @@ -328,6 +361,92 @@ for p in pkg.iter(): if not state["stopped"]: report_ok("Свойства: form и кратности корректны") +# ── 10b. structural consistency ────────────────────────────── + +struct_ok = True +for t in pkg.iter(): + if not isinstance(t.tag, str) or local(t) not in ("objectType", "typeDef"): + continue + if local(t) == "typeDef" and t.get(f"{{{XSI_NS}}}type") != "ObjectType": + continue + tn = t.get("name") or "(анонимный тип)" + prop_names = set() + for c in t: + if not isinstance(c.tag, str) or local(c) != "property": + continue + pn = c.get("name") + if pn: + if pn in prop_names: + report_error(f'{tn} : дублирующееся имя свойства "{pn}"') + struct_ok = False + prop_names.add(pn) + if state["stopped"]: + break + +for p_ in pkg.iter(): + if not isinstance(p_.tag, str) or local(p_) != "property": + continue + pn = p_.get("name") or p_.get("ref") + if p_.get("name") is not None and p_.get("ref") is not None: + report_error(f'Свойство "{pn}": заданы одновременно name и ref — допустимо только одно') + struct_ok = False + inline = next((c for c in p_ if isinstance(c.tag, str) and local(c) == "typeDef"), None) + if inline is not None and p_.get("type") is not None: + report_error(f'Свойство "{pn}": заданы одновременно type и вложенный — допустимо только одно') + struct_ok = False + if inline is not None and inline.get(f"{{{XSI_NS}}}type") is None: + report_error(f'Свойство "{pn}": у вложенного не задан xsi:type (ValueType или ObjectType)') + struct_ok = False + if state["stopped"]: + break + +# Анонимный тип внутри valueType задаёт базовый тип и xsi:type не несёт +for vt in pkg.iter(): + if not isinstance(vt.tag, str) or local(vt) != "valueType": + continue + for c in vt: + if isinstance(c.tag, str) and local(c) == "typeDef" and c.get(f"{{{XSI_NS}}}type") is not None: + report_warn(f'{vt.get("name")} : у внутри задан xsi:type — ' + "платформа его здесь не пишет") + +# Род базового типа должен совпадать +for t in pkg.iter(): + if not isinstance(t.tag, str) or t.get("base") is None: + continue + kind = local(t) + if kind not in ("objectType", "valueType"): + continue + parts = t.get("base").split(":") + if len(parts) != 2: + continue + if t.nsmap.get(parts[0]) != target_ns: + continue + if kind == "objectType" and parts[1] in value_type_names: + report_error(f'{t.get("name")} : base="{t.get("base")}" ссылается на valueType, ' + "а objectType может наследоваться только от objectType") + struct_ok = False + if kind == "valueType" and parts[1] in object_type_names: + report_error(f'{t.get("name")} : base="{t.get("base")}" ссылается на objectType, ' + "а valueType может строиться только на простом типе") + struct_ok = False + +# Union без состава +for t in pkg.iter(): + if not isinstance(t.tag, str) or local(t) not in ("valueType", "typeDef"): + continue + if t.get("variety") != "Union" or t.get("memberTypes") is not None: + continue + if not any(isinstance(c.tag, str) and local(c) == "typeDef" for c in t): + tn = t.get("name") or "(анонимный тип)" + report_warn(f'{tn} : variety="Union" без memberTypes и без вложенных типов — состав объединения пуст') + +if struct_ok and not state["stopped"]: + report_ok("Структура типов и свойств согласована") + +if state["stopped"]: + finalize() + sys.exit(1) + # ── 11. metadata object ────────────────────────────────────── if md_path: @@ -375,8 +494,8 @@ if os.path.exists(config_xml): except Exception: # noqa: BLE001 pass if clash: - report_error(f'targetNamespace "{target_ns}" уже занят пакет(ами): {", ".join(clash)}. ' - "Платформа не допускает два пакета с одним пространством имён") + report_warn(f'targetNamespace "{target_ns}" объявлен также в пакет(ах): {", ".join(clash)}. ' + "Платформа это допускает, но на это пространство имён становится неоднозначным") else: report_ok("targetNamespace уникален в конфигурации") else: diff --git a/docs/xdto-dsl-spec.md b/docs/xdto-dsl-spec.md index 016dab7a..91bf8ad7 100644 --- a/docs/xdto-dsl-spec.md +++ b/docs/xdto-dsl-spec.md @@ -169,5 +169,25 @@ - **`form="qualified"` на глобальном объявлении** — так экспортирует XML-схему сам Конфигуратор, хотя спецификация XSD допускает `form` только у локальных объявлений. Такие файлы читаются; сам навык пишет корректно. -- **`xs:include`** игнорируется (в модели XDTO соответствия нет). -- Порядок объявлений верхнего уровня произвольный и сохраняется как есть. +- **Порядок объявлений верхнего уровня произвольный** — компилятор расставляет их + в требуемом моделью порядке `import → property → valueType → objectType`. +- **`xs:group` и `xs:attributeGroup`** раскрываются по ссылке: содержимое группы + подставляется в тип. + +## 5. Конструкции без точного соответствия + +XML Schema выразительнее модели XDTO. Перечисленное ниже переносится приближённо, +и компилятор об этом **предупреждает** — молча терять свойства нельзя. + +| Конструкция | Что происходит | +|---|---| +| вложенные `xs:sequence` / `xs:choice` | уплощаются в плоский список свойств; вложенный выбор варианта не сохраняется | +| `xs:all` | трактуется как последовательность | +| `minOccurs`/`maxOccurs` на самой частице | не выражается, отбрасывается | +| `substitutionGroup` | объявление сохраняется как обычное | +| `xs:key`, `xs:keyref`, `xs:unique` | отбрасываются | +| `xs:redefine` | игнорируется | +| `xs:include` | игнорируется: зависимости разрешаются только по namespace — включаемую схему нужно собрать отдельным пакетом и заменить на `xs:import` | + +`ordered="false"` (выбор одного из вариантов) выводится только из **корневой** `xs:choice` +типа: модель хранит признак на типе целиком, а не на вложенной частице. diff --git a/tests/skills/cases/xdto-compile/nested-particles.json b/tests/skills/cases/xdto-compile/nested-particles.json new file mode 100644 index 00000000..49a7dcbb --- /dev/null +++ b/tests/skills/cases/xdto-compile/nested-particles.json @@ -0,0 +1,14 @@ +{ + "name": "вложенные частицы и группы: уплощение с предупреждениями, ни одно свойство не потеряно", + "caseFiles": ["nested-particles.xsd"], + "params": { "xsdFile": "nested-particles.xsd" }, + "validatePath": "XDTOPackages/nested_particles", + "expect": { + "files": ["XDTOPackages/nested_particles/Ext/Package.bin"], + "stdoutContains": [ + "вложенная xs:choice уплощена", + "кратность на вложенной частице", + "xs:all трактуется как последовательность" + ] + } +} diff --git a/tests/skills/cases/xdto-compile/nested-particles.xsd b/tests/skills/cases/xdto-compile/nested-particles.xsd new file mode 100644 index 00000000..6903de43 --- /dev/null +++ b/tests/skills/cases/xdto-compile/nested-particles.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/skills/cases/xdto-compile/snapshots/nested-particles/Configuration.xml b/tests/skills/cases/xdto-compile/snapshots/nested-particles/Configuration.xml new file mode 100644 index 00000000..c06bdb13 --- /dev/null +++ b/tests/skills/cases/xdto-compile/snapshots/nested-particles/Configuration.xml @@ -0,0 +1,252 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + UUID-012 + UUID-013 + + + UUID-014 + UUID-015 + + + + TestConfig + + + ru + TestConfig + + + + + Version8_3_24 + ManagedApplication + + PlatformApplication + + Russian + + + + + false + false + false + + + + + + + + + + + + + + + + + + + + + + Biometrics + true + + + Location + false + + + BackgroundLocation + false + + + BluetoothPrinters + false + + + WiFiPrinters + false + + + Contacts + false + + + Calendars + false + + + PushNotifications + false + + + LocalNotifications + false + + + InAppPurchases + false + + + PersonalComputerFileExchange + false + + + Ads + false + + + NumberDialing + false + + + CallProcessing + false + + + CallLog + false + + + AutoSendSMS + false + + + ReceiveSMS + false + + + SMSLog + false + + + Camera + false + + + Microphone + false + + + MusicLibrary + false + + + PictureAndVideoLibraries + false + + + AudioPlaybackAndVibration + false + + + BackgroundAudioPlaybackAndVibration + false + + + InstallPackages + false + + + OSBackup + true + + + ApplicationUsageStatistics + false + + + BarcodeScanning + false + + + BackgroundAudioRecording + false + + + AllFilesAccess + false + + + Videoconferences + false + + + NFC + false + + + DocumentScanning + false + + + SpeechToText + false + + + Geofences + false + + + IncomingShareRequests + false + + + AllIncomingShareRequestsTypesProcessing + false + + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + TaxiEnableVersion8_2 + DontUse + Version8_3_24 + + + + Русский + nested_particles + + + \ No newline at end of file diff --git a/tests/skills/cases/xdto-compile/snapshots/nested-particles/Ext/ClientApplicationInterface.xml b/tests/skills/cases/xdto-compile/snapshots/nested-particles/Ext/ClientApplicationInterface.xml new file mode 100644 index 00000000..3c1161b2 --- /dev/null +++ b/tests/skills/cases/xdto-compile/snapshots/nested-particles/Ext/ClientApplicationInterface.xml @@ -0,0 +1,18 @@ + + + + + UUID-002 + + + + + UUID-004 + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/xdto-compile/snapshots/nested-particles/Languages/Русский.xml b/tests/skills/cases/xdto-compile/snapshots/nested-particles/Languages/Русский.xml new file mode 100644 index 00000000..37c60d78 --- /dev/null +++ b/tests/skills/cases/xdto-compile/snapshots/nested-particles/Languages/Русский.xml @@ -0,0 +1,16 @@ + + + + + Русский + + + ru + Русский + + + + ru + + + \ No newline at end of file diff --git a/tests/skills/cases/xdto-compile/snapshots/nested-particles/XDTOPackages/nested_particles.xml b/tests/skills/cases/xdto-compile/snapshots/nested-particles/XDTOPackages/nested_particles.xml new file mode 100644 index 00000000..c5df089f --- /dev/null +++ b/tests/skills/cases/xdto-compile/snapshots/nested-particles/XDTOPackages/nested_particles.xml @@ -0,0 +1,16 @@ + + + + + nested_particles + + + ru + nested_particles + + + + urn:t + + + \ No newline at end of file diff --git a/tests/skills/cases/xdto-compile/snapshots/nested-particles/XDTOPackages/nested_particles/Ext/Package.bin b/tests/skills/cases/xdto-compile/snapshots/nested-particles/XDTOPackages/nested_particles/Ext/Package.bin new file mode 100644 index 00000000..9b22c019 --- /dev/null +++ b/tests/skills/cases/xdto-compile/snapshots/nested-particles/XDTOPackages/nested_particles/Ext/Package.bin @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/xdto-compile/snapshots/nested-particles/nested-particles.xsd b/tests/skills/cases/xdto-compile/snapshots/nested-particles/nested-particles.xsd new file mode 100644 index 00000000..6903de43 --- /dev/null +++ b/tests/skills/cases/xdto-compile/snapshots/nested-particles/nested-particles.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/skills/cases/xdto-validate/pkg-badorder/XDTOPackages/badorder.xml b/tests/skills/cases/xdto-validate/pkg-badorder/XDTOPackages/badorder.xml new file mode 100644 index 00000000..5b804c87 --- /dev/null +++ b/tests/skills/cases/xdto-validate/pkg-badorder/XDTOPackages/badorder.xml @@ -0,0 +1,11 @@ + + + + + badorder + rubadorder + + urn:test:badorder + + + diff --git a/tests/skills/cases/xdto-validate/pkg-badorder/XDTOPackages/badorder/Ext/Package.bin b/tests/skills/cases/xdto-validate/pkg-badorder/XDTOPackages/badorder/Ext/Package.bin new file mode 100644 index 00000000..18455c62 --- /dev/null +++ b/tests/skills/cases/xdto-validate/pkg-badorder/XDTOPackages/badorder/Ext/Package.bin @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/xdto-validate/pkg-conflict/XDTOPackages/conflict.xml b/tests/skills/cases/xdto-validate/pkg-conflict/XDTOPackages/conflict.xml new file mode 100644 index 00000000..cf197b78 --- /dev/null +++ b/tests/skills/cases/xdto-validate/pkg-conflict/XDTOPackages/conflict.xml @@ -0,0 +1,11 @@ + + + + + conflict + ruconflict + + urn:test:conflict + + + diff --git a/tests/skills/cases/xdto-validate/pkg-conflict/XDTOPackages/conflict/Ext/Package.bin b/tests/skills/cases/xdto-validate/pkg-conflict/XDTOPackages/conflict/Ext/Package.bin new file mode 100644 index 00000000..dc51209a --- /dev/null +++ b/tests/skills/cases/xdto-validate/pkg-conflict/XDTOPackages/conflict/Ext/Package.bin @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/xdto-validate/silent-anytype.json b/tests/skills/cases/xdto-validate/silent-anytype.json index bc1eef90..bf85e821 100644 --- a/tests/skills/cases/xdto-validate/silent-anytype.json +++ b/tests/skills/cases/xdto-validate/silent-anytype.json @@ -8,7 +8,7 @@ "expect": { "stdoutContains": [ "тихая деградация", - "ни один тип из этого пространства имён не используется" + "признак неразрешённой зависимости" ] }, "noSnapshot": "навык только читает — сравнивать нечего, проверка через stdoutContains" diff --git a/tests/skills/cases/xdto-validate/structural-conflicts.json b/tests/skills/cases/xdto-validate/structural-conflicts.json new file mode 100644 index 00000000..6a835bf1 --- /dev/null +++ b/tests/skills/cases/xdto-validate/structural-conflicts.json @@ -0,0 +1,18 @@ +{ + "name": "конфликты объявлений: name+ref, дубль свойства, typeDef без xsi:type, база другого рода", + "caseFiles": [ + "pkg-conflict/XDTOPackages/conflict.xml", + "pkg-conflict/XDTOPackages/conflict/Ext/Package.bin" + ], + "params": { "packagePath": "pkg-conflict/XDTOPackages/conflict" }, + "expectError": true, + "expect": { + "stdoutContains": [ + "заданы одновременно name и ref", + "дублирующееся имя свойства", + "не задан xsi:type", + "ссылается на valueType" + ] + }, + "noSnapshot": "навык только читает — проверка через stdoutContains" +} diff --git a/tests/skills/cases/xdto-validate/top-level-order.json b/tests/skills/cases/xdto-validate/top-level-order.json new file mode 100644 index 00000000..c8ee5a6f --- /dev/null +++ b/tests/skills/cases/xdto-validate/top-level-order.json @@ -0,0 +1,13 @@ +{ + "name": "нарушен порядок верхнего уровня — платформа отвергнет пакет", + "caseFiles": [ + "pkg-badorder/XDTOPackages/badorder.xml", + "pkg-badorder/XDTOPackages/badorder/Ext/Package.bin" + ], + "params": { "packagePath": "pkg-badorder/XDTOPackages/badorder" }, + "expectError": true, + "expect": { + "stdoutContains": ["Нарушен порядок элементов верхнего уровня"] + }, + "noSnapshot": "навык только читает — проверка через stdoutContains" +}