diff --git a/.claude/skills/skd-validate/SKILL.md b/.claude/skills/skd-validate/SKILL.md index 7bfea969..48d6b622 100644 --- a/.claude/skills/skd-validate/SKILL.md +++ b/.claude/skills/skd-validate/SKILL.md @@ -10,7 +10,7 @@ allowed-tools: # /skd-validate — валидация СКД (DataCompositionSchema) -Проверяет структурную корректность Template.xml схемы компоновки данных. Выявляет ошибки формата, битые ссылки, дубликаты имён. +Проверяет структурную корректность Template.xml схемы компоновки данных. Выявляет ошибки формата, битые ссылки, дубликаты имён, невалидный XDTO в `` / `` (отсутствие префикса `xs:`, несовпадение типа и квалификаторов, литералы вроде `_` в `DesignTimeValue`). ## Параметры diff --git a/.claude/skills/skd-validate/scripts/skd-validate.ps1 b/.claude/skills/skd-validate/scripts/skd-validate.ps1 index 940d71a2..781c0405 100644 --- a/.claude/skills/skd-validate/scripts/skd-validate.ps1 +++ b/.claude/skills/skd-validate/scripts/skd-validate.ps1 @@ -1,4 +1,4 @@ -# skd-validate v1.1 — Validate 1C DCS structure +# skd-validate v1.2 — Validate 1C DCS structure # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -734,6 +734,160 @@ if ($variantNodes.Count -eq 0) { } } +# --- 16. valueType structural checks --- +# Catches broken XDTO that XML/structural checks miss (decimal without xs:, +# missing qualifiers, mismatched qualifier blocks, unknown sign/length tokens). + +$validTypeQualifier = @{ + 'xs:decimal' = 'v8:NumberQualifiers' + 'xs:string' = 'v8:StringQualifiers' + 'xs:dateTime' = 'v8:DateQualifiers' + 'xs:boolean' = '' + 'v8:StandardPeriod' = '' + 'v8:UUID' = '' +} +$validSign = @('Any', 'Nonnegative', 'Negative') +$validLength = @('Variable', 'Fixed') +$validFractions = @('Date', 'DateTime', 'Time') + +$valueTypeNodes = $root.SelectNodes("//s:valueType", $ns) +$vtChecked = 0 +$vtOk = $true +foreach ($vt in $valueTypeNodes) { + $vtChecked++ + # Walk children in document order + $children = @($vt.ChildNodes | Where-Object { $_.NodeType -eq 'Element' }) + $lastType = $null # short form like 'xs:decimal' or '' (ref types resolved to '') + + foreach ($child in $children) { + $qName = "$($child.Prefix):$($child.LocalName)" + if ($child.LocalName -eq 'Type' -and $child.NamespaceURI -eq 'http://v8.1c.ru/8.1/data/core') { + $t = $child.InnerText.Trim() + if (-not $t) { + Report-Error "valueType: is empty" + $vtOk = $false + $lastType = $null + continue + } + # Must have a known prefix — xs:, v8:, or any prefix bound to current-config namespace + if ($t -match '^([A-Za-z][A-Za-z0-9]*):(.+)$') { + $prefix = $Matches[1] + $localName = $Matches[2] + $lastType = $t + if ($prefix -eq 'xs' -or $prefix -eq 'v8') { + if (-not $validTypeQualifier.ContainsKey($t)) { + Report-Error "valueType: unknown type '$t' (allowed: xs:decimal/xs:string/xs:dateTime/xs:boolean/v8:StandardPeriod or :*Ref.X)" + $vtOk = $false + $lastType = $null + } + } else { + # Inline-declared prefix — should resolve to current-config namespace + $prefixNs = $child.GetNamespaceOfPrefix($prefix) + if ($prefixNs -ne 'http://v8.1c.ru/8.1/data/enterprise/current-config') { + Report-Error "valueType: type '$t' uses prefix '$prefix' which is not bound to enterprise/current-config namespace" + $vtOk = $false + $lastType = $null + } elseif (-not ($localName -match '^[A-Za-z]+(Ref)?\.')) { + Report-Error "valueType: ref type '$t' must look like ':.' (e.g. d5p1:CatalogRef.X)" + $vtOk = $false + $lastType = '' + } else { + $lastType = '' # ref type — no qualifier expected + } + } + } else { + Report-Error "valueType: type '$t' has no namespace prefix (expected xs:/v8:/d5p1: — e.g. xs:decimal not decimal)" + $vtOk = $false + $lastType = $null + } + } elseif ($child.LocalName -match 'Qualifiers$' -and $child.NamespaceURI -eq 'http://v8.1c.ru/8.1/data/core') { + # Qualifier block — must match preceding Type + $expected = if ($lastType -and $validTypeQualifier.ContainsKey($lastType)) { + $validTypeQualifier[$lastType] + } else { $null } + + if ($null -eq $expected -or $expected -eq '') { + Report-Error "valueType: <$qName> after $lastType — this type has no qualifiers" + $vtOk = $false + } elseif ($qName -ne $expected) { + Report-Error "valueType: <$qName> doesn't match $lastType (expected <$expected>)" + $vtOk = $false + } else { + # Validate qualifier internals + if ($qName -eq 'v8:NumberQualifiers') { + $digits = $child.SelectSingleNode("v8:Digits", $ns) + $frac = $child.SelectSingleNode("v8:FractionDigits", $ns) + $sign = $child.SelectSingleNode("v8:AllowedSign", $ns) + if (-not $digits -or -not ($digits.InnerText -match '^\d+$')) { + Report-Error "v8:NumberQualifiers: missing or not a non-negative integer" + $vtOk = $false + } + if (-not $frac -or -not ($frac.InnerText -match '^\d+$')) { + Report-Error "v8:NumberQualifiers: missing or not a non-negative integer" + $vtOk = $false + } + if ($sign -and $sign.InnerText -and $sign.InnerText -notin $validSign) { + Report-Error "v8:NumberQualifiers: $($sign.InnerText) — must be one of: $($validSign -join ', ')" + $vtOk = $false + } + } elseif ($qName -eq 'v8:StringQualifiers') { + $len = $child.SelectSingleNode("v8:Length", $ns) + $al = $child.SelectSingleNode("v8:AllowedLength", $ns) + if (-not $len -or -not ($len.InnerText -match '^\d+$')) { + Report-Error "v8:StringQualifiers: missing or not a non-negative integer" + $vtOk = $false + } + if ($al -and $al.InnerText -and $al.InnerText -notin $validLength) { + Report-Error "v8:StringQualifiers: $($al.InnerText) — must be one of: $($validLength -join ', ')" + $vtOk = $false + } + } elseif ($qName -eq 'v8:DateQualifiers') { + $df = $child.SelectSingleNode("v8:DateFractions", $ns) + if ($df -and $df.InnerText -and $df.InnerText -notin $validFractions) { + Report-Error "v8:DateQualifiers: $($df.InnerText) — must be one of: $($validFractions -join ', ')" + $vtOk = $false + } + } + } + $lastType = $null # qualifier consumed; next must be another Type or end + } + } +} +if ($vtChecked -gt 0 -and $vtOk) { + Report-OK "$vtChecked valueType block(s): structure and qualifiers OK" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 17. value content checks --- +# Catches literal placeholders ("_") and empty strings in DesignTimeValue refs +# that XDTO would reject at db-load-xml. + +$valueNodes = @() +$valueNodes += @($root.SelectNodes("//s:value[@xsi:type]", $ns)) +$valueNodes += @($root.SelectNodes("//dcscor:value[@xsi:type]", $ns)) +$vChecked = 0 +$vOk = $true +foreach ($vn in $valueNodes) { + if (-not $vn) { continue } + $vChecked++ + $xsiType = $vn.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + $text = $vn.InnerText + if ($xsiType -eq 'dcscor:DesignTimeValue') { + if (-not $text -or $text.Trim() -eq '' -or $text.Trim() -eq '_') { + Report-Error "$text — DesignTimeValue must be a reference path (e.g. Перечисление.X.Y), not '$text'" + $vOk = $false + } elseif (-not ($text -match '^[A-Za-zА-Яа-яЁё]+\.[A-Za-zА-Яа-яЁё0-9_]+')) { + Report-Warn "$text — doesn't look like a typical ref path" + } + } +} +if ($vChecked -gt 0 -and $vOk) { + Report-OK "$vChecked element(s) with xsi:type: content OK" +} + +if ($script:stopped) { & $finalize; exit 1 } + # --- Final output --- & $finalize diff --git a/.claude/skills/skd-validate/scripts/skd-validate.py b/.claude/skills/skd-validate/scripts/skd-validate.py index e18d0382..409ad590 100644 --- a/.claude/skills/skd-validate/scripts/skd-validate.py +++ b/.claude/skills/skd-validate/scripts/skd-validate.py @@ -1,4 +1,4 @@ -# skd-validate v1.1 — Validate 1C DCS structure (Python port) +# skd-validate v1.2 — Validate 1C DCS structure (Python port) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os @@ -685,6 +685,146 @@ else: if v_ok: report_ok(f"{len(variant_nodes)} settingsVariant(s) found") +# ── 16. valueType structural checks ─────────────────────────── +# Catches broken XDTO that XML/structural checks miss (decimal without xs:, +# missing qualifiers, mismatched qualifier blocks, unknown sign/length tokens). + +import re as _re_vt + +_VALID_TYPE_QUALIFIER = { + 'xs:decimal': 'v8:NumberQualifiers', + 'xs:string': 'v8:StringQualifiers', + 'xs:dateTime': 'v8:DateQualifiers', + 'xs:boolean': '', + 'v8:StandardPeriod': '', + 'v8:UUID': '', +} +_VALID_SIGN = ('Any', 'Nonnegative', 'Negative') +_VALID_LENGTH = ('Variable', 'Fixed') +_VALID_FRACTIONS = ('Date', 'DateTime', 'Time') +_V8_NS_URI = 'http://v8.1c.ru/8.1/data/core' +_CONFIG_NS_URI = 'http://v8.1c.ru/8.1/data/enterprise/current-config' + +vt_nodes = find_all(root, "//s:valueType") +vt_checked = 0 +vt_ok = True +for vt in vt_nodes: + vt_checked += 1 + last_type = None # short form 'xs:decimal' or '' (ref — no qualifiers) + for child in vt: + if not isinstance(child.tag, str): + continue # comments etc. + qname_local = etree.QName(child.tag).localname + qname_ns = etree.QName(child.tag).namespace + + if qname_local == 'Type' and qname_ns == _V8_NS_URI: + t = (child.text or '').strip() + if not t: + report_error("valueType: is empty") + vt_ok = False + last_type = None + continue + m = _re_vt.match(r'^([A-Za-z][A-Za-z0-9]*):(.+)$', t) + if not m: + report_error(f"valueType: type '{t}' has no namespace prefix (expected xs:/v8:/d5p1: — e.g. xs:decimal not decimal)") + vt_ok = False + last_type = None + continue + prefix, local = m.group(1), m.group(2) + last_type = t + if prefix in ('xs', 'v8'): + if t not in _VALID_TYPE_QUALIFIER: + report_error(f"valueType: unknown type '{t}' (allowed: xs:decimal/xs:string/xs:dateTime/xs:boolean/v8:StandardPeriod or :*Ref.X)") + vt_ok = False + last_type = None + else: + # Inline-declared prefix — must resolve to current-config namespace + prefix_ns = child.nsmap.get(prefix) + if prefix_ns != _CONFIG_NS_URI: + report_error(f"valueType: type '{t}' uses prefix '{prefix}' which is not bound to enterprise/current-config namespace") + vt_ok = False + last_type = None + elif not _re_vt.match(r'^[A-Za-z]+(Ref)?\.', local): + report_error(f"valueType: ref type '{t}' must look like ':.' (e.g. d5p1:CatalogRef.X)") + vt_ok = False + last_type = '' + else: + last_type = '' # ref — no qualifier expected + + elif qname_local.endswith('Qualifiers') and qname_ns == _V8_NS_URI: + q_name = f"v8:{qname_local}" + expected = _VALID_TYPE_QUALIFIER.get(last_type) if last_type else None + if expected is None or expected == '': + report_error(f"valueType: <{q_name}> after {last_type} — this type has no qualifiers") + vt_ok = False + elif q_name != expected: + report_error(f"valueType: <{q_name}> doesn't match {last_type} (expected <{expected}>)") + vt_ok = False + else: + if q_name == 'v8:NumberQualifiers': + digits = find(child, "v8:Digits") + frac = find(child, "v8:FractionDigits") + sign = find(child, "v8:AllowedSign") + if digits is None or not _re_vt.match(r'^\d+$', text_of(digits)): + report_error("v8:NumberQualifiers: missing or not a non-negative integer") + vt_ok = False + if frac is None or not _re_vt.match(r'^\d+$', text_of(frac)): + report_error("v8:NumberQualifiers: missing or not a non-negative integer") + vt_ok = False + if sign is not None and text_of(sign) and text_of(sign) not in _VALID_SIGN: + report_error(f"v8:NumberQualifiers: {text_of(sign)} — must be one of: {', '.join(_VALID_SIGN)}") + vt_ok = False + elif q_name == 'v8:StringQualifiers': + length = find(child, "v8:Length") + al = find(child, "v8:AllowedLength") + if length is None or not _re_vt.match(r'^\d+$', text_of(length)): + report_error("v8:StringQualifiers: missing or not a non-negative integer") + vt_ok = False + if al is not None and text_of(al) and text_of(al) not in _VALID_LENGTH: + report_error(f"v8:StringQualifiers: {text_of(al)} — must be one of: {', '.join(_VALID_LENGTH)}") + vt_ok = False + elif q_name == 'v8:DateQualifiers': + df = find(child, "v8:DateFractions") + if df is not None and text_of(df) and text_of(df) not in _VALID_FRACTIONS: + report_error(f"v8:DateQualifiers: {text_of(df)} — must be one of: {', '.join(_VALID_FRACTIONS)}") + vt_ok = False + last_type = None # consumed + +if vt_checked > 0 and vt_ok: + report_ok(f"{vt_checked} valueType block(s): structure and qualifiers OK") + +if stopped: + finalize() + sys.exit(1) + +# ── 17. value content checks ────────────────────────────────── +# Catches literal placeholders ('_') and empty strings in DesignTimeValue refs +# that XDTO would reject at db-load-xml. + +value_nodes = find_all(root, "//s:value[@xsi:type]") + find_all(root, "//dcscor:value[@xsi:type]") +v_checked = 0 +v_ok = True +for vn in value_nodes: + if vn is None: + continue + v_checked += 1 + xsi_type = vn.get(XSI_TYPE) or '' + text = vn.text or '' + if xsi_type == 'dcscor:DesignTimeValue': + stripped = text.strip() + if not stripped or stripped == '_': + report_error(f"{text} — DesignTimeValue must be a reference path (e.g. Перечисление.X.Y), not '{text}'") + v_ok = False + elif not _re_vt.match(r'^[A-Za-zА-Яа-яЁё]+\.[A-Za-zА-Яа-яЁё0-9_]+', stripped): + report_warn(f"{text} — doesn't look like a typical ref path") + +if v_checked > 0 and v_ok: + report_ok(f"{v_checked} element(s) with xsi:type: content OK") + +if stopped: + finalize() + sys.exit(1) + # ── Final output ────────────────────────────────────────────── finalize() diff --git a/tests/skills/cases/skd-validate/error-allowed-sign.json b/tests/skills/cases/skd-validate/error-allowed-sign.json new file mode 100644 index 00000000..34f0226e --- /dev/null +++ b/tests/skills/cases/skd-validate/error-allowed-sign.json @@ -0,0 +1,6 @@ +{ + "name": "Ошибка: AllowedSign со значением вне Any/Nonnegative/Negative", + "setup": "fixture:bad-allowed-sign", + "params": { "templatePath": "Template.xml" }, + "expectError": true +} diff --git a/tests/skills/cases/skd-validate/error-decimal-no-qualifiers.json b/tests/skills/cases/skd-validate/error-decimal-no-qualifiers.json new file mode 100644 index 00000000..c2e66d9a --- /dev/null +++ b/tests/skills/cases/skd-validate/error-decimal-no-qualifiers.json @@ -0,0 +1,6 @@ +{ + "name": "Ошибка: xs:decimal с неполными NumberQualifiers (нет Digits/FractionDigits)", + "setup": "fixture:bad-decimal-no-qualifiers", + "params": { "templatePath": "Template.xml" }, + "expectError": true +} diff --git a/tests/skills/cases/skd-validate/error-decimal-no-xs.json b/tests/skills/cases/skd-validate/error-decimal-no-xs.json new file mode 100644 index 00000000..1e2afa33 --- /dev/null +++ b/tests/skills/cases/skd-validate/error-decimal-no-xs.json @@ -0,0 +1,6 @@ +{ + "name": "Ошибка: decimal без префикса xs:", + "setup": "fixture:bad-decimal-no-xs", + "params": { "templatePath": "Template.xml" }, + "expectError": true +} diff --git a/tests/skills/cases/skd-validate/error-qualifier-mismatch.json b/tests/skills/cases/skd-validate/error-qualifier-mismatch.json new file mode 100644 index 00000000..ad41e127 --- /dev/null +++ b/tests/skills/cases/skd-validate/error-qualifier-mismatch.json @@ -0,0 +1,6 @@ +{ + "name": "Ошибка: xs:string с NumberQualifiers (несоответствие тип/qualifiers)", + "setup": "fixture:bad-qualifier-mismatch", + "params": { "templatePath": "Template.xml" }, + "expectError": true +} diff --git a/tests/skills/cases/skd-validate/error-ref-literal.json b/tests/skills/cases/skd-validate/error-ref-literal.json new file mode 100644 index 00000000..bfbe9b6a --- /dev/null +++ b/tests/skills/cases/skd-validate/error-ref-literal.json @@ -0,0 +1,6 @@ +{ + "name": "Ошибка: DesignTimeValue со значением '_' (BUG-2 от titan)", + "setup": "fixture:bad-ref-literal", + "params": { "templatePath": "Template.xml" }, + "expectError": true +} diff --git a/tests/skills/cases/skd-validate/fixtures/bad-allowed-sign/Template.xml b/tests/skills/cases/skd-validate/fixtures/bad-allowed-sign/Template.xml new file mode 100644 index 00000000..1fa7df29 --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/bad-allowed-sign/Template.xml @@ -0,0 +1,35 @@ + + + + ИсточникДанных1 + Local + + + Основной + ИсточникДанных1 + SELECT 1 + + Сумма + Сумма + + xs:decimal + + 10 + 2 + Garbage + + + + + + Основной + + + diff --git a/tests/skills/cases/skd-validate/fixtures/bad-decimal-no-qualifiers/Template.xml b/tests/skills/cases/skd-validate/fixtures/bad-decimal-no-qualifiers/Template.xml new file mode 100644 index 00000000..35f37f78 --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/bad-decimal-no-qualifiers/Template.xml @@ -0,0 +1,33 @@ + + + + ИсточникДанных1 + Local + + + Основной + ИсточникДанных1 + SELECT 1 + + Сумма + Сумма + + xs:decimal + + Any + + + + + + Основной + + + diff --git a/tests/skills/cases/skd-validate/fixtures/bad-decimal-no-xs/Template.xml b/tests/skills/cases/skd-validate/fixtures/bad-decimal-no-xs/Template.xml new file mode 100644 index 00000000..cb031e98 --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/bad-decimal-no-xs/Template.xml @@ -0,0 +1,30 @@ + + + + ИсточникДанных1 + Local + + + Основной + ИсточникДанных1 + SELECT 1 + + Сумма + Сумма + + decimal + + + + + Основной + + + diff --git a/tests/skills/cases/skd-validate/fixtures/bad-qualifier-mismatch/Template.xml b/tests/skills/cases/skd-validate/fixtures/bad-qualifier-mismatch/Template.xml new file mode 100644 index 00000000..714044f2 --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/bad-qualifier-mismatch/Template.xml @@ -0,0 +1,35 @@ + + + + ИсточникДанных1 + Local + + + Основной + ИсточникДанных1 + SELECT 1 + + Текст + Текст + + xs:string + + 10 + 2 + Any + + + + + + Основной + + + diff --git a/tests/skills/cases/skd-validate/fixtures/bad-ref-literal/Template.xml b/tests/skills/cases/skd-validate/fixtures/bad-ref-literal/Template.xml new file mode 100644 index 00000000..76b312a3 --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/bad-ref-literal/Template.xml @@ -0,0 +1,34 @@ + + + + ИсточникДанных1 + Local + + + Основной + ИсточникДанных1 + SELECT 1 + + Поле1 + Поле1 + + + + Организация + + d5p1:CatalogRef.Организации + + _ + + + Основной + + +