diff --git a/.claude/skills/cfe-validate/SKILL.md b/.claude/skills/cfe-validate/SKILL.md index 6a1cff2e..d1c610e9 100644 --- a/.claude/skills/cfe-validate/SKILL.md +++ b/.claude/skills/cfe-validate/SKILL.md @@ -1,7 +1,7 @@ --- name: cfe-validate description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности -argument-hint: [-Detailed] [-MaxErrors 30] +argument-hint: [-ConfigPath ] [-Detailed] [-MaxErrors 30] allowed-tools: - Bash - Read @@ -17,13 +17,17 @@ allowed-tools: | Параметр | Обяз. | Умолч. | Описание | |---------------|:-----:|---------|-------------------------------------------------| | ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения | +| ConfigPath | нет | — | Конфигурация-источник: включает сверку путей заимствованных форм с реквизитами и табличными частями объектов основной конфигурации | | Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | | MaxErrors | нет | 30 | Остановиться после N ошибок | | OutFile | нет | — | Записать результат в файл | +Без `-ConfigPath` проверка путей пропускается — об этом сказано в отчёте отдельной строкой. Путь к конфигурации ищи так же, как в `/cfe-borrow`: `.v8-project.json` → поле `configSrc` целевой базы. + ## Команда ```powershell powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml" +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src" -ConfigPath "C:\cfsrc\erp" ``` diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 index b956f0bd..e005432f 100644 --- a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 @@ -1,4 +1,4 @@ -# cfe-validate v1.7 — Validate 1C configuration extension structure (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы) +# cfe-validate v1.8 — Validate 1C configuration extension structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -9,7 +9,11 @@ param( [int]$MaxErrors = 30, - [string]$OutFile + [string]$OutFile, + + # Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией, + # пропускаются (о чём сказано в отчёте), остальные работают как раньше. + [string]$ConfigPath ) $ErrorActionPreference = "Stop" @@ -144,6 +148,46 @@ $childTypeDirMap = @{ "IntegrationService"="IntegrationServices" } +# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации). +# Неполный набор в заимствованной оболочке платформа отвергает при загрузке: «отсутствует один +# или более типов объекта <Тип>». Типы, у которых GeneratedType нет вовсе (общие модули, +# подписки, регламентные задания и т.п.), в карте отсутствуют — для них проверка не выполняется. +$generatedTypeCategories = @{ + "Catalog" = @("Object","Ref","Selection","List","Manager") + "Document" = @("Object","Ref","Selection","List","Manager") + "Enum" = @("Ref","Manager","List") + "Constant" = @("Manager","ValueManager","ValueKey") + "Report" = @("Object","Manager") + "DataProcessor" = @("Object","Manager") + "ExchangePlan" = @("Object","Ref","Selection","List","Manager") + "Task" = @("Object","Ref","Selection","List","Manager") + "BusinessProcess" = @("Object","Ref","Selection","List","Manager","RoutePointRef") + "ChartOfCharacteristicTypes" = @("Object","Ref","Selection","List","Manager","Characteristic") + "ChartOfAccounts" = @("Object","Ref","Selection","List","Manager","ExtDimensionTypes","ExtDimensionTypesRow") + "ChartOfCalculationTypes" = @("Object","Ref","Selection","List","Manager","DisplacingCalculationTypes","DisplacingCalculationTypesRow","BaseCalculationTypes","BaseCalculationTypesRow","LeadingCalculationTypes","LeadingCalculationTypesRow") + "InformationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","RecordManager") + "AccumulationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey") + "AccountingRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","ExtDimensions") + "CalculationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","Recalcs") + "DocumentJournal" = @("Selection","List","Manager") + "Sequence" = @("Record","Manager","RecordSet") + "FilterCriterion" = @("Manager","List") + "SettingsStorage" = @("Manager") + "IntegrationService" = @("Manager") + "WSReference" = @("Manager") + "DefinedType" = @("DefinedType") +} + +# Стандартные реквизиты объектов: в ChildObjects их нет, но пути Объект.<Стандартный> законны. +# Имена зависят от варианта встроенного языка, поэтому держим оба написания. +$script:standardObjectFields = @( + "Code","Description","Ref","Parent","Owner","DeletionMark","Predefined","IsFolder","LineNumber", + "Number","Date","Posted","PredefinedDataName","RegisterRecords","DataVersion","RowsCount", + "Код","Наименование","Ссылка","Родитель","Владелец","ПометкаУдаления","Предопределенный", + "ЭтоГруппа","НомерСтроки","Номер","Дата","Проведен","ИмяПредопределенныхДанных", + "Движения","ВерсияДанных","КоличествоСтрок" +) + # Valid enum values for extension properties $validEnumValues = @{ "ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1") @@ -537,6 +581,7 @@ if ($script:stopped) { & $finalize; exit 1 } # --- Check 9: Borrowed objects validation + Check 10: Sub-items --- $script:enumValuesIndex = @{} +$script:borrowedTSIndex = @{} $script:formList = @() # Helper: check if sub-item has explicit borrowed metadata @@ -640,6 +685,25 @@ if ($childObjNode) { } else { $borrowedOk++ } + + # Полнота набора GeneratedType: платформа отвергает оболочку с неполным набором + # («отсутствует один или более типов объекта ChartOfCharacteristicTypes») + $expectedCats = $generatedTypeCategories[$typeName] + if ($expectedCats) { + $objInfo = $objEl.SelectSingleNode("md:InternalInfo", $objNs) + $foundCats = @{} + if ($objInfo) { + foreach ($gt in $objInfo.SelectNodes("xr:GeneratedType", $objNs)) { + $cat = $gt.GetAttribute("category") + if ($cat) { $foundCats[$cat] = $true } + } + } + $missingCats = @($expectedCats | Where-Object { -not $foundCats.ContainsKey($_) }) + if ($missingCats.Count -gt 0) { + Report-Error "9. Borrowed ${typeName}.${childName}: missing GeneratedType categor$(if ($missingCats.Count -eq 1) { 'y' } else { 'ies' }) $($missingCats -join ', ')" + $check9Ok = $false + } + } } # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- @@ -667,6 +731,12 @@ if ($childObjNode) { $tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs) $tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) $tsLabel = if ($tsName) { $tsName.InnerText } else { "?" } + # Индекс заимствованных ТЧ — по нему Check 12 сверяет + if ($tsName) { + $tsKey = "${typeName}.${childName}" + if (-not $script:borrowedTSIndex.ContainsKey($tsKey)) { $script:borrowedTSIndex[$tsKey] = @{} } + $script:borrowedTSIndex[$tsKey][$tsName.InnerText] = $true + } if (-not $tsInfo) { Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo" $check10Ok = $false @@ -896,6 +966,28 @@ foreach ($bf in $script:borrowedFormsWithTree) { } } + # — доп. колонки табличной части, объявленные в самой форме. + # Колонки есть, а самой ТЧ в расширении нет → платформа отвергает загрузку: «Неверный путь к + # данным» плюс «Колонки не могут быть добавлены к реквизиту». + $acTables = @{} + foreach ($m in [regex]::Matches($raw, ' — TabularSection.${tblName} not borrowed in extension" + $check12Ok = $false + } + } + } + foreach ($mi in $missingItems) { Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension" $check12Ok = $false @@ -931,6 +1023,114 @@ if ($script:borrowedFormsWithTree.Count -eq 0) { Report-OK "13. TypeLink: clean" } +# --- Check 14: пути Объект.* заимствованных форм против конфигурации-источника --- +# Требует -ConfigPath: отличить живой путь от висячего можно только по исходному объекту. +# «Объект.Партнер» валиден и без заимствования реквизита (наследуется от базы), а «Объект.Товары.Артикул» +# не разрешится нигде, если Артикул — не реквизит объекта и не колонка из самой формы. +# Такой путь платформа отвергает на загрузке: «Неверный путь к данным». +if (-not $script:stopped -and $script:borrowedFormsWithTree.Count -gt 0) { + if (-not $ConfigPath) { + Out-Line "[INFO] 14. Пути Объект.* против конфигурации-источника не проверялись: не задан -ConfigPath" + } else { + $cfgRoot = $ConfigPath + if (-not [System.IO.Path]::IsPathRooted($cfgRoot)) { $cfgRoot = Join-Path (Get-Location).Path $cfgRoot } + if ((Test-Path $cfgRoot) -and -not (Test-Path $cfgRoot -PathType Container)) { $cfgRoot = Split-Path $cfgRoot -Parent } + + if (-not (Test-Path (Join-Path $cfgRoot "Configuration.xml"))) { + Report-Warn "14. -ConfigPath '$ConfigPath': Configuration.xml не найден — проверка путей пропущена" + } else { + $check14Ok = $true + $pathCheckCount = 0 + + foreach ($bf in $script:borrowedFormsWithTree) { + $raw = $bf.RawText + $ctx = $bf.Context + $ownerKey = ($ctx -split '\.Form\.')[0] + $ownerParts = $ownerKey -split '\.', 2 + if ($ownerParts.Count -lt 2) { continue } + $ownerType = $ownerParts[0]; $ownerName = $ownerParts[1] + $ownerDir = $childTypeDirMap[$ownerType] + if (-not $ownerDir) { continue } + $srcObjFile = Join-Path (Join-Path $cfgRoot $ownerDir) "${ownerName}.xml" + if (-not (Test-Path $srcObjFile)) { + Report-Warn "14. ${ctx}: объект-источник не найден в конфигурации ($ownerDir/${ownerName}.xml)" + continue + } + + # Имена, доступные первым сегментом пути: реквизиты и ТЧ объекта-источника. + # Плюс для каждой ТЧ — её колонки: второй сегмент проверяем по ним (именно там + # и жил дефект — Объект.Товары.Артикул при живой ТЧ Товары). + $srcNames = @{} + $srcTSColumns = @{} + $srcDoc = New-Object System.Xml.XmlDocument + $srcDoc.PreserveWhitespace = $false + $srcDoc.Load($srcObjFile) + $srcObjEl = $null + foreach ($c in $srcDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $srcObjEl = $c; break } + } + $srcChildObjects = if ($srcObjEl) { $srcObjEl.SelectSingleNode("*[local-name()='ChildObjects']") } else { $null } + if ($srcChildObjects) { + foreach ($sub in $srcChildObjects.ChildNodes) { + if ($sub.NodeType -ne 'Element') { continue } + if ($sub.LocalName -notin @('Attribute','TabularSection')) { continue } + $nameNode = $sub.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']") + if (-not $nameNode) { continue } + $subName = $nameNode.InnerText.Trim() + $srcNames[$subName] = $true + if ($sub.LocalName -ne 'TabularSection') { continue } + $cols = @{} + foreach ($colName in $sub.SelectNodes("*[local-name()='ChildObjects']/*[local-name()='Attribute']/*[local-name()='Properties']/*[local-name()='Name']")) { + $cols[$colName.InnerText.Trim()] = $true + } + $srcTSColumns[$subName] = $cols + } + } + # Плюс колонки, объявленные в самой форме через / + foreach ($acm in [regex]::Matches($raw, '(?s)(.*?)')) { + $tbl = $acm.Groups[1].Value + if (-not $srcTSColumns.ContainsKey($tbl)) { $srcTSColumns[$tbl] = @{} } + foreach ($cm in [regex]::Matches($acm.Groups[2].Value, ']*>Объект\.([^<]+)')) { + $segments = $m.Groups[1].Value -split '\.' + $seg0 = $segments[0] + $pathCheckCount++ + if ($script:standardObjectFields -contains $seg0) { continue } + if (-not $srcNames.ContainsKey($seg0)) { + $badPaths["Объект.${seg0}"] = "у ${ownerKey} нет такого реквизита или табличной части" + continue + } + # Второй сегмент проверяем только для табличных частей: у ссылочного реквизита + # он ведёт в чужой объект, и это уже другая проверка. + if ($segments.Count -lt 2 -or -not $srcTSColumns.ContainsKey($seg0)) { continue } + $seg1 = $segments[1] + if ($script:standardObjectFields -contains $seg1) { continue } + # Итог колонки — псевдополе платформы: Total<Колонка> при живой колонке законен + if ($seg1 -like "Total*" -and $srcTSColumns[$seg0].ContainsKey($seg1.Substring(5))) { continue } + if (-not $srcTSColumns[$seg0].ContainsKey($seg1)) { + $badPaths["Объект.${seg0}.${seg1}"] = "у табличной части ${seg0} нет колонки ${seg1}, и формы её не объявляет" + } + } + foreach ($bad in ($badPaths.Keys | Sort-Object)) { + Report-Error "14. ${ctx}: путь '${bad}' — $($badPaths[$bad])" + $check14Ok = $false + } + } + + if ($check14Ok) { + Report-OK "14. Object paths vs source config: $pathCheckCount checked" + } + } + } +} + +if ($script:stopped) { & $finalize; exit 1 } + # --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here --- $extRootDir = Split-Path $resolvedPath -Parent $ctrlCount = 0 diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.py b/.claude/skills/cfe-validate/scripts/cfe-validate.py index 131f6db9..48a2c90a 100644 --- a/.claude/skills/cfe-validate/scripts/cfe-validate.py +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cfe-validate v1.7 — Validate 1C configuration extension XML structure (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы) +# cfe-validate v1.8 — Validate 1C configuration extension XML structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects.""" import sys, os, argparse, re @@ -96,6 +96,46 @@ CHILD_TYPE_DIR_MAP = { 'IntegrationService': 'IntegrationServices', } +# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации). +# Неполный набор в заимствованной оболочке платформа отвергает при загрузке: «отсутствует один +# или более типов объекта <Тип>». Типы, у которых GeneratedType нет вовсе (общие модули, +# подписки, регламентные задания и т.п.), в карте отсутствуют — для них проверка не выполняется. +GENERATED_TYPE_CATEGORIES = { + 'Catalog': ['Object', 'Ref', 'Selection', 'List', 'Manager'], + 'Document': ['Object', 'Ref', 'Selection', 'List', 'Manager'], + 'Enum': ['Ref', 'Manager', 'List'], + 'Constant': ['Manager', 'ValueManager', 'ValueKey'], + 'Report': ['Object', 'Manager'], + 'DataProcessor': ['Object', 'Manager'], + 'ExchangePlan': ['Object', 'Ref', 'Selection', 'List', 'Manager'], + 'Task': ['Object', 'Ref', 'Selection', 'List', 'Manager'], + 'BusinessProcess': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'RoutePointRef'], + 'ChartOfCharacteristicTypes': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'Characteristic'], + 'ChartOfAccounts': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'ExtDimensionTypes', 'ExtDimensionTypesRow'], + 'ChartOfCalculationTypes': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'DisplacingCalculationTypes', 'DisplacingCalculationTypesRow', 'BaseCalculationTypes', 'BaseCalculationTypesRow', 'LeadingCalculationTypes', 'LeadingCalculationTypesRow'], + 'InformationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'RecordManager'], + 'AccumulationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey'], + 'AccountingRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'ExtDimensions'], + 'CalculationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'Recalcs'], + 'DocumentJournal': ['Selection', 'List', 'Manager'], + 'Sequence': ['Record', 'Manager', 'RecordSet'], + 'FilterCriterion': ['Manager', 'List'], + 'SettingsStorage': ['Manager'], + 'IntegrationService': ['Manager'], + 'WSReference': ['Manager'], + 'DefinedType': ['DefinedType'], +} + +# Стандартные реквизиты объектов: в ChildObjects их нет, но пути Объект.<Стандартный> законны. +# Имена зависят от варианта встроенного языка, поэтому держим оба написания. +STANDARD_OBJECT_FIELDS = { + 'Code', 'Description', 'Ref', 'Parent', 'Owner', 'DeletionMark', 'Predefined', 'IsFolder', 'LineNumber', + 'Number', 'Date', 'Posted', 'PredefinedDataName', 'RegisterRecords', 'DataVersion', 'RowsCount', + 'Код', 'Наименование', 'Ссылка', 'Родитель', 'Владелец', 'ПометкаУдаления', 'Предопределенный', + 'ЭтоГруппа', 'НомерСтроки', 'Номер', 'Дата', 'Проведен', 'ИмяПредопределенныхДанных', + 'Движения', 'ВерсияДанных', 'КоличествоСтрок', +} + # Valid enum values for extension properties VALID_ENUM_VALUES = { 'ConfigurationExtensionCompatibilityMode': [ @@ -177,11 +217,15 @@ def main(): parser.add_argument('-Detailed', action='store_true') parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30) parser.add_argument('-OutFile', dest='OutFile', default='') + # Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией, + # пропускаются (о чём сказано в отчёте), остальные работают как раньше. + parser.add_argument('-ConfigPath', dest='ConfigPath', default='') args = ci_parse_args(parser) extension_path = args.ExtensionPath max_errors = args.MaxErrors out_file = args.OutFile + config_path_arg = args.ConfigPath # --- Resolve path --- if not os.path.isabs(extension_path): @@ -560,6 +604,7 @@ def main(): MD = NS['md'] XR = NS['xr'] enum_values_index = {} + borrowed_ts_index = {} form_list = [] def is_borrowed_sub_item(sub_item): @@ -659,6 +704,23 @@ def main(): else: borrowed_ok_count += 1 + # Полнота набора GeneratedType: платформа отвергает оболочку с неполным набором + # («отсутствует один или более типов объекта ChartOfCharacteristicTypes») + expected_cats = GENERATED_TYPE_CATEGORIES.get(type_name) + if expected_cats: + obj_info = obj_el.find(f'{{{MD}}}InternalInfo') + found_cats = set() + if obj_info is not None: + for gt in obj_info.findall(f'{{{XR}}}GeneratedType'): + cat = gt.get('category') + if cat: + found_cats.add(cat) + missing_cats = [c for c in expected_cats if c not in found_cats] + if missing_cats: + word = 'category' if len(missing_cats) == 1 else 'categories' + r.error(f"9. Borrowed {type_name}.{child_name}: missing GeneratedType {word} {', '.join(missing_cats)}") + check9_ok = False + # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects') if obj_child_objects is not None: @@ -686,6 +748,9 @@ def main(): ts_info = sub_item.find(f'{{{MD}}}InternalInfo') ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name') ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?' + # Индекс заимствованных ТЧ — по нему Check 12 сверяет + if ts_name_el is not None and ts_name_el.text: + borrowed_ts_index.setdefault(f'{type_name}.{child_name}', {})[ts_name_el.text.strip()] = True if ts_info is None: r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo') check10_ok = False @@ -878,6 +943,22 @@ def main(): elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}): missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}") + # — доп. колонки табличной части, объявленные в самой форме. + # Колонки есть, а самой ТЧ в расширении нет → платформа отвергает загрузку: «Неверный путь к + # данным» плюс «Колонки не могут быть добавлены к реквизиту». + # Соседние проверки этого блока эвристичны (имя стиля добывается регуляркой), поэтому там + # предупреждение. Здесь сигнал точный — имя ТЧ берётся из атрибута, — а последствие жёсткое, + # поэтому ошибка. + ac_tables = set(re.findall(r' — TabularSection.{tbl_name} not borrowed in extension') + check12_ok = False + for mi in missing_items: r.warn(f'12. {ctx}: references {mi} not borrowed in extension') check12_ok = False @@ -909,6 +990,115 @@ def main(): elif check13_ok: r.ok('13. TypeLink: clean') + # --- Check 14: пути Объект.* заимствованных форм против конфигурации-источника --- + # Требует -ConfigPath: отличить живой путь от висячего можно только по исходному объекту. + # «Объект.Партнер» валиден и без заимствования реквизита (наследуется от базы), а «Объект.Товары.Артикул» + # не разрешится нигде, если Артикул — не колонка ТЧ и не колонка из самой формы. + # Такой путь платформа отвергает на загрузке: «Неверный путь к данным». + if not r.stopped and borrowed_forms_with_tree: + if not config_path_arg: + r.out('[INFO] 14. Пути Объект.* против конфигурации-источника не проверялись: не задан -ConfigPath') + else: + cfg_root = config_path_arg + if not os.path.isabs(cfg_root): + cfg_root = os.path.join(os.getcwd(), cfg_root) + if os.path.exists(cfg_root) and not os.path.isdir(cfg_root): + cfg_root = os.path.dirname(cfg_root) + + if not os.path.isfile(os.path.join(cfg_root, 'Configuration.xml')): + r.warn(f"14. -ConfigPath '{config_path_arg}': Configuration.xml не найден — проверка путей пропущена") + else: + check14_ok = True + path_check_count = 0 + + for bf in borrowed_forms_with_tree: + raw = bf['RawText'] + ctx = bf['Context'] + owner_key = ctx.split('.Form.')[0] + owner_parts = owner_key.split('.', 1) + if len(owner_parts) < 2: + continue + owner_type, owner_name = owner_parts + owner_dir = CHILD_TYPE_DIR_MAP.get(owner_type) + if not owner_dir: + continue + src_obj_file = os.path.join(cfg_root, owner_dir, f'{owner_name}.xml') + if not os.path.isfile(src_obj_file): + r.warn(f'14. {ctx}: объект-источник не найден в конфигурации ({owner_dir}/{owner_name}.xml)') + continue + + # Имена, доступные первым сегментом пути: реквизиты и ТЧ объекта-источника. + # Плюс для каждой ТЧ — её колонки: второй сегмент проверяем по ним (именно там + # и жил дефект — Объект.Товары.Артикул при живой ТЧ Товары). + src_names = set() + src_ts_columns = {} + src_tree = etree.parse(src_obj_file, etree.XMLParser(remove_blank_text=True)) + src_obj_el = None + for c in src_tree.getroot(): + if isinstance(c.tag, str): + src_obj_el = c + break + src_child_objects = src_obj_el.find(f'{{{MD}}}ChildObjects') if src_obj_el is not None else None + if src_child_objects is not None: + for sub in src_child_objects: + if not isinstance(sub.tag, str): + continue + sub_ln = etree.QName(sub.tag).localname + if sub_ln not in ('Attribute', 'TabularSection'): + continue + name_el = sub.find(f'{{{MD}}}Properties/{{{MD}}}Name') + if name_el is None or not name_el.text: + continue + sub_name = name_el.text.strip() + src_names.add(sub_name) + if sub_ln != 'TabularSection': + continue + cols = set() + for col_name in sub.findall(f'{{{MD}}}ChildObjects/{{{MD}}}Attribute/{{{MD}}}Properties/{{{MD}}}Name'): + if col_name.text: + cols.add(col_name.text.strip()) + src_ts_columns[sub_name] = cols + # Плюс колонки, объявленные в самой форме через / + for acm in re.finditer(r'(.*?)', raw, re.DOTALL): + tbl = acm.group(1) + cols = src_ts_columns.setdefault(tbl, set()) + for cm in re.finditer(r']*>Объект\.([^<]+)', raw): + segments = m.group(1).split('.') + seg0 = segments[0] + path_check_count += 1 + if seg0 in STANDARD_OBJECT_FIELDS: + continue + if seg0 not in src_names: + bad_paths[f'Объект.{seg0}'] = f'у {owner_key} нет такого реквизита или табличной части' + continue + # Второй сегмент проверяем только для табличных частей: у ссылочного реквизита + # он ведёт в чужой объект, и это уже другая проверка. + if len(segments) < 2 or seg0 not in src_ts_columns: + continue + seg1 = segments[1] + if seg1 in STANDARD_OBJECT_FIELDS: + continue + # Итог колонки — псевдополе платформы: Total<Колонка> при живой колонке законен + if seg1.startswith('Total') and seg1[5:] in src_ts_columns[seg0]: + continue + if seg1 not in src_ts_columns[seg0]: + bad_paths[f'Объект.{seg0}.{seg1}'] = f'у табличной части {seg0} нет колонки {seg1}, и формы её не объявляет' + + for bad in sorted(bad_paths): + r.error(f"14. {ctx}: путь '{bad}' — {bad_paths[bad]}") + check14_ok = False + + if check14_ok: + r.ok(f'14. Object paths vs source config: {path_check_count} checked') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + # --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here --- ctrl_count = 0 for dp, _dn, files in os.walk(config_dir): diff --git a/tests/skills/cases/cfe-validate/additional-columns-missing-ts.json b/tests/skills/cases/cfe-validate/additional-columns-missing-ts.json new file mode 100644 index 00000000..2b8c8c8c --- /dev/null +++ b/tests/skills/cases/cfe-validate/additional-columns-missing-ts.json @@ -0,0 +1,48 @@ +{ + "name": "Ошибка: при незаимствованной табличной части", + "preRun": [ + { + "script": "meta-compile/scripts/meta-compile", + "input": { + "type": "Document", "name": "ЗаказКлиента", + "tabularSections": { "Товары": [ { "name": "Количество", "type": "Number(10,3)" } ] } + }, + "args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" } + }, + { + "script": "form-add/scripts/form-add", + "args": { "-ObjectPath": "{workDir}/Documents/ЗаказКлиента.xml", "-FormName": "ФормаДокумента" } + }, + { + "script": "form-compile/scripts/form-compile", + "input": { + "title": "Заказ клиента", + "elements": [ + { "table": "Товары", "path": "Объект.Товары", "columns": [ + { "input": "Количество", "path": "Объект.Товары.Количество" } + ] } + ], + "attributes": [ + { + "name": "Объект", "type": "DocumentObject.ЗаказКлиента", "main": true, + "additionalColumns": [ + { "table": "Объект.Оплата", "columns": [ { "name": "СуммаПрописью", "type": "String(200)" } ] } + ] + } + ] + }, + "args": { "-JsonPath": "{inputFile}", "-OutputPath": "{workDir}/Documents/ЗаказКлиента/Forms/ФормаДокумента/Ext/Form.xml" } + }, + { + "script": "cfe-init/scripts/cfe-init", + "args": { "-Name": "Тест", "-OutputDir": "{workDir}/ext", "-ConfigPath": "{workDir}" } + }, + { + "script": "cfe-borrow/scripts/cfe-borrow", + "args": { "-ExtensionPath": "{workDir}/ext", "-ConfigPath": "{workDir}", "-Object": "Document.ЗаказКлиента.Form.ФормаДокумента", "-BorrowMainAttribute": "Form" } + } + ], + "params": { "extensionPath": "ext" }, + "expectError": true, + "expect": { "stdoutContains": "TabularSection.Оплата" } +} diff --git a/tests/skills/cases/cfe-validate/object-paths-vs-config.json b/tests/skills/cases/cfe-validate/object-paths-vs-config.json new file mode 100644 index 00000000..f96e6793 --- /dev/null +++ b/tests/skills/cases/cfe-validate/object-paths-vs-config.json @@ -0,0 +1,43 @@ +{ + "name": "С -ConfigPath ловится путь на несуществующую колонку ТЧ (Объект.Товары.Артикул)", + "preRun": [ + { + "script": "meta-compile/scripts/meta-compile", + "input": { + "type": "Document", "name": "ЗаказКлиента", + "tabularSections": { "Товары": [ { "name": "Количество", "type": "Number(10,3)" } ] } + }, + "args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" } + }, + { + "script": "form-add/scripts/form-add", + "args": { "-ObjectPath": "{workDir}/Documents/ЗаказКлиента.xml", "-FormName": "ФормаДокумента" } + }, + { + "script": "form-compile/scripts/form-compile", + "input": { + "title": "Заказ клиента", + "elements": [ + { "table": "Товары", "path": "Объект.Товары", "columns": [ + { "input": "Количество", "path": "Объект.Товары.Количество" }, + { "input": "Артикул", "path": "Объект.Товары.Артикул" } + ] } + ], + "attributes": [ { "name": "Объект", "type": "DocumentObject.ЗаказКлиента", "main": true } ] + }, + "args": { "-JsonPath": "{inputFile}", "-OutputPath": "{workDir}/Documents/ЗаказКлиента/Forms/ФормаДокумента/Ext/Form.xml" } + }, + { + "script": "cfe-init/scripts/cfe-init", + "args": { "-Name": "Тест", "-OutputDir": "{workDir}/ext", "-ConfigPath": "{workDir}" } + }, + { + "script": "cfe-borrow/scripts/cfe-borrow", + "args": { "-ExtensionPath": "{workDir}/ext", "-ConfigPath": "{workDir}", "-Object": "Document.ЗаказКлиента.Form.ФормаДокумента", "-BorrowMainAttribute": "Form" } + } + ], + "params": { "extensionPath": "ext" }, + "args_extra": ["-ConfigPath", "{workDir}"], + "expectError": true, + "expect": { "stdoutContains": "Объект.Товары.Артикул" } +} diff --git a/tests/skills/check-type-maps.mjs b/tests/skills/check-type-maps.mjs index ac4cf824..52e6966b 100644 --- a/tests/skills/check-type-maps.mjs +++ b/tests/skills/check-type-maps.mjs @@ -85,6 +85,10 @@ const MAPS = [ IntegrationService: '', }, }, + { + skill: 'cfe-validate', file: 'cfe-validate', kind: 'gencats', + py: 'GENERATED_TYPE_CATEGORIES', ps1: '$generatedTypeCategories', + }, { skill: 'meta-validate', file: 'meta-validate', kind: 'gencats', py: 'generated_type_categories', ps1: '$generatedTypeCategories', @@ -204,8 +208,8 @@ function extractGen(text, name, kind, lang) { cur = h[1]; out.set(cur, []); if (kind === 'gencats') { - // категории лежат в той же строке: "Catalog" = @("Object","Ref",…) - for (const c of line.slice(h[0].length).matchAll(/"(\w+)"/g)) out.get(cur).push([null, c[1]]); + // категории лежат в той же строке: "Catalog" = @("Object","Ref",…) / 'Catalog': ['Object', …] + for (const c of line.slice(h[0].length).matchAll(/['"](\w+)['"]/g)) out.get(cur).push([null, c[1]]); } continue; }