diff --git a/.claude/skills/db-list/SKILL.md b/.claude/skills/db-list/SKILL.md index db5c5786..1669159b 100644 --- a/.claude/skills/db-list/SKILL.md +++ b/.claude/skills/db-list/SKILL.md @@ -38,7 +38,7 @@ allowed-tools: "user": "Admin", "password": "", "aliases": ["dev", "разработка"], - "branches": ["dev", "develop"], + "branches": ["dev", "develop", "feature/*"], "configSrc": "C:\\WS\\myapp\\cfsrc" }, { @@ -77,22 +77,26 @@ allowed-tools: | `user` | string | нет | Имя пользователя 1С | | `password` | string | нет | Пароль | | `aliases` | string[] | нет | Альтернативные имена для быстрого доступа | -| `branches` | string[] | нет | Git-ветки, привязанные к этой базе | +| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе | | `configSrc` | string | нет | Каталог XML-выгрузки конфигурации | ## Алгоритм разрешения базы данных -Этот алгоритм используется ВСЕМИ навыками `db-*` для определения целевой базы. +Этот алгоритм используется ВСЕМИ навыками (`db-*`, `epf-build`, `epf-dump`, `erf-build`, `erf-dump`) для определения целевой базы. -1. Прочитай `.v8-project.json` (в корне проекта или ближайшем родительском каталоге) -2. Если пользователь указал базу — ищи совпадение в таком порядке: +1. Если пользователь указал **параметры подключения** (путь, сервер) — используй напрямую +2. Если пользователь указал **базу по имени** — ищи совпадение в таком порядке: 1. По `id` (точное совпадение) 2. По `aliases` (совпадение в массиве с учётом морфологии: «тестовую» = «тестовая» = «тестовой») - 3. По `branches` (совпадение с текущей Git-веткой) - 4. По `name` (нечёткое совпадение с учётом морфологии и регистра) -3. Если пользователь не указал базу — используй `default` -4. Если не найдено или неоднозначно — спроси пользователя -5. Если файл `.v8-project.json` не найден — спроси параметры подключения и предложи создать файл + 3. По `name` (нечёткое совпадение с учётом морфологии и регистра) +3. Если пользователь **не указал** базу — сопоставь текущую ветку Git с `databases[].branches`: + - Точное совпадение: ветка `dev` → `"branches": ["dev"]` + - Glob-паттерн: ветка `release/2.1` → `"branches": ["release/*"]` +4. Если ветка не совпала — используй `default` +5. Если не найдено или неоднозначно — спроси пользователя +6. Если файл `.v8-project.json` не найден — спроси параметры подключения и предложи создать файл + +После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`. ### Автоопределение платформы diff --git a/.claude/skills/epf-validate/SKILL.md b/.claude/skills/epf-validate/SKILL.md new file mode 100644 index 00000000..7193632b --- /dev/null +++ b/.claude/skills/epf-validate/SKILL.md @@ -0,0 +1,88 @@ +--- +name: epf-validate +description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности +argument-hint: [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /epf-validate — валидация внешней обработки (EPF) + +Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. + +Скрипт также работает для внешних отчётов (ERF) — автоопределение по типу элемента. См. `/erf-validate`. + +## Использование + +``` +/epf-validate +``` + +## Параметры + +| Параметр | Обязательный | По умолчанию | Описание | +|------------|:------------:|--------------|-------------------------------------------------| +| ObjectPath | да | — | Путь к корневому XML или каталогу обработки | +| MaxErrors | нет | 30 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | + +`ObjectPath` авторезолв: если указана директория — ищет `/.xml`. + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\epf-validate\scripts\epf-validate.ps1 -ObjectPath "<путь>" +``` + +## Выполняемые проверки + +| # | Проверка | Серьёзность | +|----|-------------------------------------------------------|--------------| +| 1 | Root structure: MetaDataObject/ExternalDataProcessor | ERROR | +| 2 | InternalInfo: ClassId, ContainedObject, GeneratedType | ERROR / WARN | +| 3 | Properties: Name (identifier), Synonym | ERROR / WARN | +| 4 | ChildObjects: допустимые типы, порядок | ERROR / WARN | +| 5 | Cross-references: DefaultForm → Form, AuxiliaryForm | ERROR / WARN | +| 6 | Attributes: UUID, Name, Type | ERROR | +| 7 | TabularSections: UUID, Name, GeneratedType, Attributes | ERROR / WARN | +| 8 | Уникальность имён (Attribute, TS, Form, Template, Command) | ERROR | +| 9 | Файлы: формы (.xml + Ext/Form.xml), макеты | ERROR | +| 10 | Дескрипторы форм: корневая структура, uuid, Name, FormType | ERROR / WARN | + +## Вывод + +``` +=== Validation: EPF.МояОбработка === + +[OK] 1. Root structure: MetaDataObject/ExternalDataProcessor, version 2.17 +[OK] 2. InternalInfo: ClassId correct, 1 GeneratedType +[OK] 3. Properties: Name="МояОбработка", Synonym present, DefaultForm set +[OK] 4. ChildObjects: Attribute(3), TabularSection(1), Form(1) +[OK] 5. Cross-references: DefaultForm valid +[OK] 6. Attributes: 3 checked (UUID, Name, Type) +[OK] 7. TabularSections: 1 sections, 5 inner attributes +[OK] 8. Name uniqueness: 6 names, all unique +[OK] 9. File existence: 3 files verified +[OK] 10. Form descriptors: 1 checked + +=== Result: 0 errors, 0 warnings === +``` + +Код возврата: 0 = все проверки пройдены, 1 = есть ошибки. + +## Верификация + +``` +/epf-init — создать обработку +/epf-validate src/.xml — проверить результат +/epf-build — собрать EPF +``` + +## Когда использовать + +- **После `/epf-init`**: проверить scaffold +- **После добавления формы/макета**: убедиться что ChildObjects, файлы и ссылки корректны +- **После ручного редактирования XML**: выявить структурные ошибки до сборки +- **При отладке сборки**: найти причину ошибки Designer diff --git a/.claude/skills/epf-validate/scripts/epf-validate.ps1 b/.claude/skills/epf-validate/scripts/epf-validate.ps1 new file mode 100644 index 00000000..95c5c2d6 --- /dev/null +++ b/.claude/skills/epf-validate/scripts/epf-validate.ps1 @@ -0,0 +1,824 @@ +# epf-validate v1.0 — Validate 1C external data processor / report structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects +param( + [Parameter(Mandatory)] + [string]$ObjectPath, + + [int]$MaxErrors = 30, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- + +if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) { + $ObjectPath = Join-Path (Get-Location).Path $ObjectPath +} + +if (Test-Path $ObjectPath -PathType Container) { + $dirName = Split-Path $ObjectPath -Leaf + $candidate = Join-Path $ObjectPath "$dirName.xml" + if (Test-Path $candidate) { + $ObjectPath = $candidate + } else { + $xmlFiles = @(Get-ChildItem $ObjectPath -Filter "*.xml" -File | Select-Object -First 1) + if ($xmlFiles.Count -gt 0) { + $ObjectPath = $xmlFiles[0].FullName + } else { + Write-Host "[ERROR] No XML file found in directory: $ObjectPath" + exit 1 + } + } +} + +if (-not (Test-Path $ObjectPath)) { + Write-Host "[ERROR] File not found: $ObjectPath" + exit 1 +} + +$resolvedPath = (Resolve-Path $ObjectPath).Path +$srcDir = Split-Path $resolvedPath -Parent + +# --- Output infrastructure --- + +$script:errors = 0 +$script:warnings = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 8192 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + Out-Line "[OK] $msg" +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ===" + + $result = $script:output.ToString() + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +# --- Reference tables --- + +$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' +$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' + +$classIds = @{ + "ExternalDataProcessor" = "c3831ec8-d8d5-4f93-8a22-f9bfae07327f" + "ExternalReport" = "e41aff26-25cf-4bb6-b6c1-3f478a75f374" +} + +$allowedChildTypes = @("Attribute","TabularSection","Form","Template","Command") + +# Expected order of child types in ChildObjects +$childTypeOrder = @{ + "Attribute" = 0 + "TabularSection" = 1 + "Form" = 2 + "Template" = 3 + "Command" = 4 +} + +$validPropertyValues = @{ + "FillChecking" = @("DontCheck","ShowError","ShowWarning") + "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") +} + +# --- 1. Parse XML --- + +Out-Line "" + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) +} catch { + Out-Line "=== Validation: (parse failed) ===" + Out-Line "" + Report-Error "1. XML parse failed: $($_.Exception.Message)" + & $finalize + exit 1 +} + +# --- Register namespaces --- + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") + +$root = $xmlDoc.DocumentElement + +# --- Check 1: Root structure --- + +$check1Ok = $true + +if ($root.LocalName -ne "MetaDataObject") { + Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" + & $finalize + exit 1 +} + +$expectedNs = "http://v8.1c.ru/8.3/MDClasses" +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" + $check1Ok = $false +} + +$version = $root.GetAttribute("version") +if (-not $version) { + Report-Warn "1. Missing version attribute on MetaDataObject" +} elseif ($version -ne "2.17" -and $version -ne "2.20") { + Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)" +} + +# Detect type: ExternalDataProcessor or ExternalReport +$typeNode = $null +$mdType = "" +$childElements = @() +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.NamespaceURI -eq $expectedNs) { + $childElements += $child + } +} + +if ($childElements.Count -eq 0) { + Report-Error "1. No metadata type element found inside MetaDataObject" + & $finalize + exit 1 +} elseif ($childElements.Count -gt 1) { + Report-Error "1. Multiple type elements found: $($childElements | ForEach-Object { $_.LocalName })" + $check1Ok = $false +} + +$typeNode = $childElements[0] +$mdType = $typeNode.LocalName + +if ($mdType -ne "ExternalDataProcessor" -and $mdType -ne "ExternalReport") { + Report-Error "1. Unexpected type '$mdType' (expected ExternalDataProcessor or ExternalReport)" + & $finalize + exit 1 +} + +$typeUuid = $typeNode.GetAttribute("uuid") +if (-not $typeUuid) { + Report-Error "1. Missing uuid on <$mdType>" + $check1Ok = $false +} elseif ($typeUuid -notmatch $guidPattern) { + Report-Error "1. Invalid uuid '$typeUuid' on <$mdType>" + $check1Ok = $false +} + +# Get object name +$propsNode = $typeNode.SelectSingleNode("md:Properties", $ns) +$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } +$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } + +$shortType = if ($mdType -eq "ExternalDataProcessor") { "EPF" } else { "ERF" } +$script:output.Insert(0, "=== Validation: $shortType.$objName ===$([Environment]::NewLine)") | Out-Null + +if ($check1Ok) { + Report-OK "1. Root structure: MetaDataObject/$mdType, version $version" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 2: InternalInfo --- + +$internalInfo = $typeNode.SelectSingleNode("md:InternalInfo", $ns) + +if (-not $internalInfo) { + Report-Error "2. InternalInfo block missing" +} else { + $check2Ok = $true + + # ContainedObject / ClassId + $containedObj = $internalInfo.SelectSingleNode("xr:ContainedObject", $ns) + if (-not $containedObj) { + Report-Error "2. InternalInfo: missing xr:ContainedObject" + $check2Ok = $false + } else { + $classIdNode = $containedObj.SelectSingleNode("xr:ClassId", $ns) + $objectIdNode = $containedObj.SelectSingleNode("xr:ObjectId", $ns) + + $expectedClassId = $classIds[$mdType] + if (-not $classIdNode -or -not $classIdNode.InnerText) { + Report-Error "2. Missing ClassId in ContainedObject" + $check2Ok = $false + } elseif ($classIdNode.InnerText -ne $expectedClassId) { + Report-Error "2. ClassId is '$($classIdNode.InnerText)', expected '$expectedClassId' for $mdType" + $check2Ok = $false + } + + if ($objectIdNode -and $objectIdNode.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ObjectId UUID" + $check2Ok = $false + } + } + + # GeneratedType — expect exactly 1 with category "Object" + $genTypes = $internalInfo.SelectNodes("xr:GeneratedType", $ns) + if ($genTypes.Count -eq 0) { + Report-Error "2. No GeneratedType entries found" + $check2Ok = $false + } else { + foreach ($gt in $genTypes) { + $gtName = $gt.GetAttribute("name") + $gtCategory = $gt.GetAttribute("category") + + if ($gtCategory -ne "Object") { + Report-Warn "2. Unexpected GeneratedType category '$gtCategory' (expected 'Object')" + } + + # Name format: ExternalDataProcessorObject.Name or ExternalReportObject.Name + $expectedPrefix = "${mdType}Object." + if ($gtName -and $objName -ne "(unknown)" -and -not $gtName.StartsWith($expectedPrefix)) { + Report-Warn "2. GeneratedType name '$gtName' does not start with '$expectedPrefix'" + } + + $typeId = $gt.SelectSingleNode("xr:TypeId", $ns) + $valueId = $gt.SelectSingleNode("xr:ValueId", $ns) + if ($typeId -and $typeId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid TypeId UUID in GeneratedType" + $check2Ok = $false + } + if ($valueId -and $valueId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ValueId UUID in GeneratedType" + $check2Ok = $false + } + } + } + + if ($check2Ok) { + Report-OK "2. InternalInfo: ClassId correct, $($genTypes.Count) GeneratedType" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 3: Properties --- + +if (-not $propsNode) { + Report-Error "3. Properties block missing" +} else { + $check3Ok = $true + + # Name + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "3. Properties: Name is missing or empty" + $check3Ok = $false + } else { + $nameVal = $nameNode.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "3. Properties: Name '$nameVal' is not a valid 1C identifier" + $check3Ok = $false + } + if ($nameVal.Length -gt 80) { + Report-Warn "3. Properties: Name '$nameVal' exceeds 80 characters ($($nameVal.Length))" + } + } + + # Synonym + $synNode = $propsNode.SelectSingleNode("md:Synonym", $ns) + $synPresent = $false + if ($synNode) { + $synItem = $synNode.SelectSingleNode("v8:item", $ns) + if ($synItem) { + $synContent = $synItem.SelectSingleNode("v8:content", $ns) + if ($synContent -and $synContent.InnerText) { + $synPresent = $true + } + } + } + + # DefaultForm cross-reference (collected now, checked after ChildObjects) + $defaultFormNode = $propsNode.SelectSingleNode("md:DefaultForm", $ns) + $defaultFormVal = if ($defaultFormNode -and $defaultFormNode.InnerText.Trim()) { $defaultFormNode.InnerText.Trim() } else { "" } + + # AuxiliaryForm cross-reference + $auxFormNode = $propsNode.SelectSingleNode("md:AuxiliaryForm", $ns) + $auxFormVal = if ($auxFormNode -and $auxFormNode.InnerText.Trim()) { $auxFormNode.InnerText.Trim() } else { "" } + + # ERF-specific: MainDataCompositionSchema + $mainDCSVal = "" + if ($mdType -eq "ExternalReport") { + $mainDCSNode = $propsNode.SelectSingleNode("md:MainDataCompositionSchema", $ns) + $mainDCSVal = if ($mainDCSNode -and $mainDCSNode.InnerText.Trim()) { $mainDCSNode.InnerText.Trim() } else { "" } + } + + if ($check3Ok) { + $synInfo = if ($synPresent) { "Synonym present" } else { "no Synonym" } + $extras = "" + if ($defaultFormVal) { $extras += ", DefaultForm set" } + if ($mainDCSVal) { $extras += ", MainDCS set" } + Report-OK "3. Properties: Name=`"$objName`", $synInfo$extras" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 4: ChildObjects — allowed types and ordering --- + +$childObjNode = $typeNode.SelectSingleNode("md:ChildObjects", $ns) +$formNames = @() +$templateNames = @() +$allChildNames = @{} + +if ($childObjNode) { + $check4Ok = $true + $childCounts = @{} + $lastOrder = -1 + $orderOk = $true + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childTag = $child.LocalName + + if ($allowedChildTypes -notcontains $childTag) { + Report-Error "4. ChildObjects: disallowed element '$childTag'" + $check4Ok = $false + continue + } + + if (-not $childCounts.ContainsKey($childTag)) { + $childCounts[$childTag] = 0 + } + $childCounts[$childTag]++ + + # Check ordering + $thisOrder = $childTypeOrder[$childTag] + if ($thisOrder -lt $lastOrder -and $orderOk) { + Report-Warn "4. ChildObjects: '$childTag' appears after higher-order elements (expected: Attribute, TabularSection, Form, Template, Command)" + $orderOk = $false + } + $lastOrder = $thisOrder + + # Collect Form and Template names (simple text content) + if ($childTag -eq "Form") { + $formNames += $child.InnerText.Trim() + } elseif ($childTag -eq "Template") { + $templateNames += $child.InnerText.Trim() + } + } + + if ($check4Ok) { + $summary = ($childCounts.GetEnumerator() | Sort-Object { $childTypeOrder[$_.Name] } | ForEach-Object { "$($_.Name)($($_.Value))" }) -join ", " + if ($summary) { + Report-OK "4. ChildObjects: $summary" + } else { + Report-OK "4. ChildObjects: empty" + } + } +} else { + Report-OK "4. ChildObjects: absent" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 5: DefaultForm / MainDCS cross-references --- + +$check5Ok = $true + +if ($defaultFormVal) { + # Format: ExternalDataProcessor.Name.Form.FormName or ExternalReport.Name.Form.FormName + $expectedPrefix = "$mdType.$objName.Form." + if ($defaultFormVal.StartsWith($expectedPrefix)) { + $refFormName = $defaultFormVal.Substring($expectedPrefix.Length) + if ($formNames -notcontains $refFormName) { + Report-Error "5. DefaultForm references '$refFormName', but no such Form in ChildObjects" + $check5Ok = $false + } + } else { + Report-Warn "5. DefaultForm value '$defaultFormVal' has unexpected prefix (expected '$expectedPrefix...')" + } +} + +if ($auxFormVal) { + $expectedPrefix = "$mdType.$objName.Form." + if ($auxFormVal.StartsWith($expectedPrefix)) { + $refFormName = $auxFormVal.Substring($expectedPrefix.Length) + if ($formNames -notcontains $refFormName) { + Report-Error "5. AuxiliaryForm references '$refFormName', but no such Form in ChildObjects" + $check5Ok = $false + } + } +} + +if ($mainDCSVal -and $mdType -eq "ExternalReport") { + $expectedPrefix = "ExternalReport.$objName.Template." + if ($mainDCSVal.StartsWith($expectedPrefix)) { + $refTplName = $mainDCSVal.Substring($expectedPrefix.Length) + if ($templateNames -notcontains $refTplName) { + Report-Error "5. MainDataCompositionSchema references '$refTplName', but no such Template in ChildObjects" + $check5Ok = $false + } + } else { + Report-Warn "5. MainDataCompositionSchema value '$mainDCSVal' has unexpected prefix" + } +} + +if ($check5Ok) { + $refs = @() + if ($defaultFormVal) { $refs += "DefaultForm" } + if ($auxFormVal) { $refs += "AuxiliaryForm" } + if ($mainDCSVal) { $refs += "MainDCS" } + if ($refs.Count -gt 0) { + Report-OK "5. Cross-references: $($refs -join ', ') valid" + } else { + Report-OK "5. Cross-references: none to check" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 6: Attributes — UUID, Name, Type --- + +function Check-Attribute { + param( + [System.Xml.XmlNode]$node, + [string]$context + ) + + $uuid = $node.GetAttribute("uuid") + if (-not $uuid) { + Report-Error "6. $context Attribute missing uuid" + return $false + } elseif ($uuid -notmatch $guidPattern) { + Report-Error "6. $context Attribute has invalid uuid '$uuid'" + return $false + } + + $elProps = $node.SelectSingleNode("md:Properties", $ns) + if (-not $elProps) { + Report-Error "6. $context Attribute (uuid=$uuid) missing Properties" + return $false + } + + $elName = $elProps.SelectSingleNode("md:Name", $ns) + if (-not $elName -or -not $elName.InnerText) { + Report-Error "6. $context Attribute (uuid=$uuid) missing or empty Name" + return $false + } + + $nameVal = $elName.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "6. $context Attribute '$nameVal' has invalid identifier" + return $false + } + + $typeEl = $elProps.SelectSingleNode("md:Type", $ns) + if (-not $typeEl) { + Report-Error "6. $context Attribute '$nameVal' missing Type block" + return $false + } + $v8Types = $typeEl.SelectNodes("v8:Type", $ns) + $v8TypeSets = $typeEl.SelectNodes("v8:TypeSet", $ns) + if ($v8Types.Count -eq 0 -and $v8TypeSets.Count -eq 0) { + Report-Error "6. $context Attribute '$nameVal' Type block has no v8:Type or v8:TypeSet" + return $false + } + + return $true +} + +if ($childObjNode) { + $attrs = $childObjNode.SelectNodes("md:Attribute", $ns) + $check6Ok = $true + $attrCount = 0 + + foreach ($attr in $attrs) { + if ($script:stopped) { break } + $ok = Check-Attribute -node $attr -context "" + if (-not $ok) { $check6Ok = $false } + $attrCount++ + + # Collect name for uniqueness + $ap = $attr.SelectSingleNode("md:Properties/md:Name", $ns) + if ($ap -and $ap.InnerText) { + $allChildNames["Attr:$($ap.InnerText)"] = $ap.InnerText + } + } + + if ($attrCount -gt 0) { + if ($check6Ok) { + Report-OK "6. Attributes: $attrCount checked (UUID, Name, Type)" + } + } else { + Report-OK "6. Attributes: none" + } +} else { + Report-OK "6. Attributes: N/A" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 7: TabularSections --- + +if ($childObjNode) { + $tsSections = $childObjNode.SelectNodes("md:TabularSection", $ns) + if ($tsSections.Count -gt 0) { + $check7Ok = $true + $tsCount = 0 + $tsAttrTotal = 0 + + foreach ($ts in $tsSections) { + if ($script:stopped) { break } + $tsCount++ + + $tsUuid = $ts.GetAttribute("uuid") + if (-not $tsUuid -or $tsUuid -notmatch $guidPattern) { + Report-Error "7. TabularSection #${tsCount}: invalid or missing uuid" + $check7Ok = $false + } + + $tsProps = $ts.SelectSingleNode("md:Properties", $ns) + $tsNameNode = if ($tsProps) { $tsProps.SelectSingleNode("md:Name", $ns) } else { $null } + $tsName = if ($tsNameNode -and $tsNameNode.InnerText) { $tsNameNode.InnerText } else { "(unnamed)" } + + if (-not $tsNameNode -or -not $tsNameNode.InnerText) { + Report-Error "7. TabularSection #${tsCount}: missing or empty Name" + $check7Ok = $false + } elseif ($tsName -notmatch $identPattern) { + Report-Error "7. TabularSection '$tsName': invalid identifier" + $check7Ok = $false + } + + $allChildNames["TS:$tsName"] = $tsName + + # InternalInfo — expect 2 GeneratedType + $tsIntInfo = $ts.SelectSingleNode("md:InternalInfo", $ns) + if ($tsIntInfo) { + $tsGens = $tsIntInfo.SelectNodes("xr:GeneratedType", $ns) + if ($tsGens.Count -lt 2) { + Report-Warn "7. TabularSection '$tsName': expected 2 GeneratedType, found $($tsGens.Count)" + } + } + + # Inner attributes + $tsChildObj = $ts.SelectSingleNode("md:ChildObjects", $ns) + if ($tsChildObj) { + $tsAttrs = $tsChildObj.SelectNodes("md:Attribute", $ns) + $tsAttrNames = @{} + foreach ($ta in $tsAttrs) { + $taOk = Check-Attribute -node $ta -context "TabularSection '$tsName'." + if (-not $taOk) { $check7Ok = $false } + $tsAttrTotal++ + + $taProps = $ta.SelectSingleNode("md:Properties/md:Name", $ns) + if ($taProps -and $taProps.InnerText) { + if ($tsAttrNames.ContainsKey($taProps.InnerText)) { + Report-Error "7. Duplicate attribute '$($taProps.InnerText)' in TabularSection '$tsName'" + $check7Ok = $false + } else { + $tsAttrNames[$taProps.InnerText] = $true + } + } + } + } + } + + if ($check7Ok) { + Report-OK "7. TabularSections: $tsCount sections, $tsAttrTotal inner attributes" + } + } else { + Report-OK "7. TabularSections: none" + } +} else { + Report-OK "7. TabularSections: N/A" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 8: Name uniqueness --- + +$check8Ok = $true + +# Collect all names: attributes + tabular sections + forms + templates + commands +$allNames = @{} + +if ($childObjNode) { + $nameKinds = @( + @{ XPath = "md:Attribute"; Kind = "Attribute" }, + @{ XPath = "md:TabularSection"; Kind = "TabularSection" }, + @{ XPath = "md:Command"; Kind = "Command" } + ) + + foreach ($nk in $nameKinds) { + $nodes = $childObjNode.SelectNodes($nk.XPath, $ns) + foreach ($node in $nodes) { + $np = $node.SelectSingleNode("md:Properties/md:Name", $ns) + if ($np -and $np.InnerText) { + $nameVal = $np.InnerText + $key = "$($nk.Kind):$nameVal" + if ($allNames.ContainsKey($nameVal)) { + Report-Error "8. Duplicate name '$nameVal' ($($nk.Kind) conflicts with $($allNames[$nameVal]))" + $check8Ok = $false + } else { + $allNames[$nameVal] = $nk.Kind + } + } + } + } + + # Forms and Templates are simple text nodes + foreach ($fn in $formNames) { + if ($allNames.ContainsKey($fn)) { + Report-Error "8. Duplicate name '$fn' (Form conflicts with $($allNames[$fn]))" + $check8Ok = $false + } else { + $allNames[$fn] = "Form" + } + } + foreach ($tn in $templateNames) { + if ($allNames.ContainsKey($tn)) { + Report-Error "8. Duplicate name '$tn' (Template conflicts with $($allNames[$tn]))" + $check8Ok = $false + } else { + $allNames[$tn] = "Template" + } + } +} + +if ($check8Ok) { + Report-OK "8. Name uniqueness: $($allNames.Count) names, all unique" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 9: File existence (forms and templates on disk) --- + +$check9Ok = $true +$filesChecked = 0 + +# Object directory: same level as root XML, named after the object +$objDir = Join-Path $srcDir $objName + +foreach ($fn in $formNames) { + # FormName.xml — form descriptor + $formMetaXml = Join-Path (Join-Path $objDir "Forms") "$fn.xml" + if (-not (Test-Path $formMetaXml)) { + Report-Error "9. Missing form descriptor: Forms/$fn.xml" + $check9Ok = $false + } else { + $filesChecked++ + } + + # FormName/Ext/Form.xml — form layout + $formXml = Join-Path (Join-Path (Join-Path (Join-Path $objDir "Forms") $fn) "Ext") "Form.xml" + if (-not (Test-Path $formXml)) { + Report-Error "9. Missing form layout: Forms/$fn/Ext/Form.xml" + $check9Ok = $false + } else { + $filesChecked++ + } +} + +foreach ($tn in $templateNames) { + # TemplateName.xml — template descriptor + $tplMetaXml = Join-Path (Join-Path $objDir "Templates") "$tn.xml" + if (-not (Test-Path $tplMetaXml)) { + Report-Error "9. Missing template descriptor: Templates/$tn.xml" + $check9Ok = $false + } else { + $filesChecked++ + } + + # TemplateName/Ext/Template.* — template content (extension varies) + $tplExtDir = Join-Path (Join-Path (Join-Path $objDir "Templates") $tn) "Ext" + if (Test-Path $tplExtDir) { + $tplFiles = @(Get-ChildItem $tplExtDir -Filter "Template.*" -File) + if ($tplFiles.Count -eq 0) { + Report-Error "9. Missing template content: Templates/$tn/Ext/Template.*" + $check9Ok = $false + } else { + $filesChecked++ + } + } else { + Report-Error "9. Missing template Ext directory: Templates/$tn/Ext/" + $check9Ok = $false + } +} + +# ObjectModule.bsl +$objModule = Join-Path (Join-Path $objDir "Ext") "ObjectModule.bsl" +if (Test-Path $objModule) { + $filesChecked++ +} + +if ($check9Ok) { + if ($filesChecked -gt 0) { + Report-OK "9. File existence: $filesChecked files verified" + } else { + Report-OK "9. File existence: no forms/templates to check" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 10: Form descriptors structure --- + +$check10Ok = $true +$formsChecked = 0 + +foreach ($fn in $formNames) { + $formMetaXml = Join-Path (Join-Path $objDir "Forms") "$fn.xml" + if (-not (Test-Path $formMetaXml)) { continue } + + try { + $fDoc = New-Object System.Xml.XmlDocument + $fDoc.PreserveWhitespace = $false + $fDoc.Load($formMetaXml) + $fRoot = $fDoc.DocumentElement + + if ($fRoot.LocalName -ne "MetaDataObject") { + Report-Error "10. Form '$fn': root element is '$($fRoot.LocalName)', expected 'MetaDataObject'" + $check10Ok = $false + continue + } + + $fTypeNode = $fRoot.SelectSingleNode("md:Form", $ns) + if (-not $fTypeNode) { + Report-Error "10. Form '$fn': missing
element" + $check10Ok = $false + continue + } + + $fUuid = $fTypeNode.GetAttribute("uuid") + if (-not $fUuid -or $fUuid -notmatch $guidPattern) { + Report-Error "10. Form '$fn': invalid or missing uuid" + $check10Ok = $false + } + + $fProps = $fTypeNode.SelectSingleNode("md:Properties", $ns) + if ($fProps) { + $fName = $fProps.SelectSingleNode("md:Name", $ns) + if ($fName -and $fName.InnerText -ne $fn) { + Report-Error "10. Form '$fn': Name in descriptor is '$($fName.InnerText)', expected '$fn'" + $check10Ok = $false + } + + # FormType should be Managed + $fType = $fProps.SelectSingleNode("md:FormType", $ns) + if ($fType -and $fType.InnerText -ne "Managed") { + Report-Warn "10. Form '$fn': FormType is '$($fType.InnerText)' (expected 'Managed')" + } + } + + $formsChecked++ + } catch { + Report-Error "10. Form '$fn': XML parse error: $($_.Exception.Message)" + $check10Ok = $false + } +} + +if ($check10Ok) { + if ($formsChecked -gt 0) { + Report-OK "10. Form descriptors: $formsChecked checked" + } else { + Report-OK "10. Form descriptors: none to check" + } +} + +# --- Final output --- + +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0 diff --git a/.claude/skills/erf-validate/SKILL.md b/.claude/skills/erf-validate/SKILL.md new file mode 100644 index 00000000..40e6d121 --- /dev/null +++ b/.claude/skills/erf-validate/SKILL.md @@ -0,0 +1,88 @@ +--- +name: erf-validate +description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности +argument-hint: [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /erf-validate — валидация внешнего отчёта (ERF) + +Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. + +Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport). + +## Использование + +``` +/erf-validate +``` + +## Параметры + +| Параметр | Обязательный | По умолчанию | Описание | +|------------|:------------:|--------------|-------------------------------------------------| +| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта | +| MaxErrors | нет | 30 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | + +`ObjectPath` авторезолв: если указана директория — ищет `/.xml`. + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\epf-validate\scripts\epf-validate.ps1 -ObjectPath "<путь>" +``` + +## Выполняемые проверки + +| # | Проверка | Серьёзность | +|----|-------------------------------------------------------|--------------| +| 1 | Root structure: MetaDataObject/ExternalReport | ERROR | +| 2 | InternalInfo: ClassId, ContainedObject, GeneratedType | ERROR / WARN | +| 3 | Properties: Name, Synonym, MainDataCompositionSchema | ERROR / WARN | +| 4 | ChildObjects: допустимые типы, порядок | ERROR / WARN | +| 5 | Cross-references: DefaultForm, MainDCS → Template | ERROR / WARN | +| 6 | Attributes: UUID, Name, Type | ERROR | +| 7 | TabularSections: UUID, Name, GeneratedType, Attributes | ERROR / WARN | +| 8 | Уникальность имён (Attribute, TS, Form, Template, Command) | ERROR | +| 9 | Файлы: формы (.xml + Ext/Form.xml), макеты | ERROR | +| 10 | Дескрипторы форм: корневая структура, uuid, Name, FormType | ERROR / WARN | + +## Вывод + +``` +=== Validation: ERF.МойОтчёт === + +[OK] 1. Root structure: MetaDataObject/ExternalReport, version 2.17 +[OK] 2. InternalInfo: ClassId correct, 1 GeneratedType +[OK] 3. Properties: Name="МойОтчёт", Synonym present, MainDCS set +[OK] 4. ChildObjects: Form(1), Template(1) +[OK] 5. Cross-references: DefaultForm, MainDCS valid +[OK] 6. Attributes: none +[OK] 7. TabularSections: none +[OK] 8. Name uniqueness: 2 names, all unique +[OK] 9. File existence: 4 files verified +[OK] 10. Form descriptors: 1 checked + +=== Result: 0 errors, 0 warnings === +``` + +Код возврата: 0 = все проверки пройдены, 1 = есть ошибки. + +## Верификация + +``` +/erf-init --with-skd — создать отчёт с СКД +/erf-validate src/.xml — проверить результат +/erf-build — собрать ERF +``` + +## Когда использовать + +- **После `/erf-init`**: проверить scaffold +- **После добавления формы/макета/СКД**: убедиться что ChildObjects и MainDCS корректны +- **После ручного редактирования XML**: выявить структурные ошибки до сборки +- **При отладке сборки**: найти причину ошибки Designer diff --git a/.claude/skills/meta-remove/SKILL.md b/.claude/skills/meta-remove/SKILL.md new file mode 100644 index 00000000..0196387c --- /dev/null +++ b/.claude/skills/meta-remove/SKILL.md @@ -0,0 +1,100 @@ +--- +name: meta-remove +description: Удалить объект метаданных из конфигурации 1С. Используй когда пользователь просит удалить, убрать объект из конфигурации +argument-hint: -Object +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /meta-remove — удаление объекта метаданных + +Удаляет объект из XML-выгрузки конфигурации: файлы, регистрацию в Configuration.xml, ссылки в подсистемах. + +## Использование + +``` +/meta-remove -Object +``` + +## Параметры + +| Параметр | Обязательный | Описание | +|------------|:------------:|-------------------------------------------------| +| ConfigDir | да | Корневая директория выгрузки (где Configuration.xml) | +| Object | да | Тип и имя объекта: `Catalog.Товары`, `Document.Заказ` и т.д. | +| DryRun | нет | Только показать что будет удалено, без изменений | +| KeepFiles | нет | Не удалять файлы, только дерегистрировать | + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\meta-remove\scripts\meta-remove.ps1 -ConfigDir "<путь>" -Object "Catalog.Товары" +``` + +## Что делает + +1. **Находит файлы объекта**: `{TypePlural}/{Name}.xml` и `{TypePlural}/{Name}/` (каталог с модулями, формами, макетами) +2. **Удаляет из Configuration.xml**: убирает `Name` из `` +3. **Очищает подсистемы**: рекурсивно обходит все `Subsystems/` и удаляет `Type.Name` из `` +4. **Удаляет файлы**: XML-файл и каталог объекта + +## Поддерживаемые типы + +Catalog, Document, Enum, Constant, InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task, ExchangePlan, DocumentJournal, Report, DataProcessor, CommonModule, ScheduledJob, EventSubscription, HTTPService, WebService, DefinedType, Role, Subsystem, CommonForm, CommonTemplate, CommonPicture, CommonAttribute, SessionParameter, FunctionalOption, FunctionalOptionsParameter, Sequence, FilterCriterion, SettingsStorage, XDTOPackage, WSReference, StyleItem, Language + +## Вывод + +``` +=== meta-remove: Catalog.Устаревший === + +[FOUND] Catalogs/Устаревший.xml +[FOUND] Catalogs/Устаревший/ (8 files) + +--- Configuration.xml --- +[OK] Removed Устаревший from ChildObjects +[OK] Configuration.xml saved + +--- Subsystems --- +[OK] Removed from subsystem 'Справочники' +[OK] Removed from subsystem 'НСИ' + +--- Files --- +[OK] Deleted directory: Catalogs/Устаревший/ +[OK] Deleted file: Catalogs/Устаревший.xml + +=== Done: 4 actions performed (2 subsystem references removed) === +``` + +Код возврата: 0 = успешно, 1 = ошибки. + +## Безопасность + +- **Рекомендуется**: сначала запустить с `-DryRun` для проверки +- **ВАЖНО**: перед удалением убедитесь что на объект нет ссылок в коде (типы реквизитов, запросы, вызовы модулей) +- Скрипт НЕ проверяет ссылки из кода — только структурные (Configuration.xml, подсистемы) +- Для проверки ссылок из кода используйте поиск по конфигурации: `grep -r "Type.Name" ` + +## Примеры + +```powershell +# Dry run — посмотреть что будет удалено +... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший" -DryRun + +# Удалить объект полностью +... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший" + +# Только дерегистрировать (файлы оставить) +... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Report.Старый" -KeepFiles + +# Удалить общий модуль +... -ConfigDir src -Object "CommonModule.МойМодуль" +``` + +## Когда использовать + +- **Рефакторинг**: удаление неиспользуемых объектов +- **Очистка**: удаление временных/тестовых объектов +- **Перенос**: удаление объекта перед пересозданием с другой структурой diff --git a/.claude/skills/meta-remove/scripts/meta-remove.ps1 b/.claude/skills/meta-remove/scripts/meta-remove.ps1 new file mode 100644 index 00000000..193b0c5e --- /dev/null +++ b/.claude/skills/meta-remove/scripts/meta-remove.ps1 @@ -0,0 +1,307 @@ +# meta-remove v1.0 — Remove metadata object from 1C configuration dump +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ConfigDir, + + [Parameter(Mandatory)] + [string]$Object, + + [switch]$DryRun, + + [switch]$KeepFiles +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Type → plural directory mapping --- + +$typePluralMap = @{ + "Catalog" = "Catalogs" + "Document" = "Documents" + "Enum" = "Enums" + "Constant" = "Constants" + "InformationRegister" = "InformationRegisters" + "AccumulationRegister" = "AccumulationRegisters" + "AccountingRegister" = "AccountingRegisters" + "CalculationRegister" = "CalculationRegisters" + "ChartOfAccounts" = "ChartsOfAccounts" + "ChartOfCharacteristicTypes" = "ChartsOfCharacteristicTypes" + "ChartOfCalculationTypes" = "ChartsOfCalculationTypes" + "BusinessProcess" = "BusinessProcesses" + "Task" = "Tasks" + "ExchangePlan" = "ExchangePlans" + "DocumentJournal" = "DocumentJournals" + "Report" = "Reports" + "DataProcessor" = "DataProcessors" + "CommonModule" = "CommonModules" + "ScheduledJob" = "ScheduledJobs" + "EventSubscription" = "EventSubscriptions" + "HTTPService" = "HTTPServices" + "WebService" = "WebServices" + "DefinedType" = "DefinedTypes" + "Role" = "Roles" + "Subsystem" = "Subsystems" + "CommonForm" = "CommonForms" + "CommonTemplate" = "CommonTemplates" + "CommonPicture" = "CommonPictures" + "CommonAttribute" = "CommonAttributes" + "SessionParameter" = "SessionParameters" + "FunctionalOption" = "FunctionalOptions" + "FunctionalOptionsParameter" = "FunctionalOptionsParameters" + "Sequence" = "Sequences" + "FilterCriterion" = "FilterCriteria" + "SettingsStorage" = "SettingsStorages" + "XDTOPackage" = "XDTOPackages" + "WSReference" = "WSReferences" + "StyleItem" = "StyleItems" + "Language" = "Languages" +} + +# --- Resolve paths --- + +if (-not [System.IO.Path]::IsPathRooted($ConfigDir)) { + $ConfigDir = Join-Path (Get-Location).Path $ConfigDir +} + +if (-not (Test-Path $ConfigDir -PathType Container)) { + Write-Host "[ERROR] Config directory not found: $ConfigDir" + exit 1 +} + +$configXml = Join-Path $ConfigDir "Configuration.xml" +if (-not (Test-Path $configXml)) { + Write-Host "[ERROR] Configuration.xml not found in: $ConfigDir" + exit 1 +} + +# --- Parse object spec --- + +$parts = $Object -split "\.", 2 +if ($parts.Count -ne 2 -or -not $parts[0] -or -not $parts[1]) { + Write-Host "[ERROR] Invalid object format '$Object'. Expected: Type.Name (e.g. Catalog.Товары)" + exit 1 +} + +$objType = $parts[0] +$objName = $parts[1] + +if (-not $typePluralMap.ContainsKey($objType)) { + Write-Host "[ERROR] Unknown type '$objType'. Supported: $($typePluralMap.Keys -join ', ')" + exit 1 +} + +$typePlural = $typePluralMap[$objType] + +Write-Host "=== meta-remove: $objType.$objName ===" +Write-Host "" + +if ($DryRun) { + Write-Host "[DRY-RUN] No changes will be made" + Write-Host "" +} + +$actions = 0 +$errors = 0 + +# --- 1. Find object files --- + +$typeDir = Join-Path $ConfigDir $typePlural +$objXml = Join-Path $typeDir "$objName.xml" +$objDir = Join-Path $typeDir $objName + +$hasXml = Test-Path $objXml +$hasDir = Test-Path $objDir -PathType Container + +if (-not $hasXml -and -not $hasDir) { + Write-Host "[WARN] Object files not found: $typePlural/$objName.xml" + Write-Host " Proceeding with deregistration only..." +} else { + if ($hasXml) { Write-Host "[FOUND] $typePlural/$objName.xml" } + if ($hasDir) { + $fileCount = @(Get-ChildItem $objDir -Recurse -File).Count + Write-Host "[FOUND] $typePlural/$objName/ ($fileCount files)" + } +} + +# --- 2. Remove from Configuration.xml ChildObjects --- + +Write-Host "" +Write-Host "--- Configuration.xml ---" + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $true +$xmlDoc.Load($configXml) + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") + +$cfgNode = $xmlDoc.DocumentElement.SelectSingleNode("md:Configuration", $ns) +if (-not $cfgNode) { + Write-Host "[ERROR] Configuration element not found in Configuration.xml" + $errors++ +} else { + $childObjects = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) + if ($childObjects) { + $found = $false + foreach ($child in @($childObjects.ChildNodes)) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq $objType -and $child.InnerText.Trim() -eq $objName) { + $found = $true + if (-not $DryRun) { + # Remove preceding whitespace if present + $prev = $child.PreviousSibling + if ($prev -and $prev.NodeType -eq 'Whitespace') { + $childObjects.RemoveChild($prev) | Out-Null + } + $childObjects.RemoveChild($child) | Out-Null + } + Write-Host "[OK] Removed <$objType>$objName from ChildObjects" + $actions++ + break + } + } + if (-not $found) { + Write-Host "[WARN] <$objType>$objName not found in ChildObjects" + } + } + + # Save Configuration.xml + if ($actions -gt 0 -and -not $DryRun) { + $enc = New-Object System.Text.UTF8Encoding $true + $sw = New-Object System.IO.StreamWriter($configXml, $false, $enc) + $xmlDoc.Save($sw) + $sw.Close() + Write-Host "[OK] Configuration.xml saved" + } +} + +# --- 3. Remove from subsystem Content --- + +Write-Host "" +Write-Host "--- Subsystems ---" + +$subsystemsDir = Join-Path $ConfigDir "Subsystems" +$subsystemsFound = 0 +$subsystemsCleaned = 0 + +function Remove-FromSubsystems { + param([string]$dir) + + $xmlFiles = @(Get-ChildItem $dir -Filter "*.xml" -File -ErrorAction SilentlyContinue) + foreach ($xmlFile in $xmlFiles) { + $ssDoc = New-Object System.Xml.XmlDocument + $ssDoc.PreserveWhitespace = $true + try { $ssDoc.Load($xmlFile.FullName) } catch { continue } + + $ssNs = New-Object System.Xml.XmlNamespaceManager($ssDoc.NameTable) + $ssNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $ssNs.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") + + $ssNode = $ssDoc.DocumentElement.SelectSingleNode("md:Subsystem", $ssNs) + if (-not $ssNode) { continue } + + $propsNode = $ssNode.SelectSingleNode("md:Properties", $ssNs) + if (-not $propsNode) { continue } + + $contentNode = $propsNode.SelectSingleNode("md:Content", $ssNs) + if (-not $contentNode) { continue } + + $ssNameNode = $propsNode.SelectSingleNode("md:Name", $ssNs) + $ssName = if ($ssNameNode) { $ssNameNode.InnerText } else { $xmlFile.BaseName } + + # Content items are Type.Name + $targetRef = "$objType.$objName" + $modified = $false + + foreach ($item in @($contentNode.ChildNodes)) { + if ($item.NodeType -ne 'Element') { continue } + $val = $item.InnerText.Trim() + # Content format: "Subsystem.X" or "Catalog.X" etc. + if ($val -eq $targetRef) { + $script:subsystemsFound++ + if (-not $DryRun) { + $prev = $item.PreviousSibling + if ($prev -and $prev.NodeType -eq 'Whitespace') { + $contentNode.RemoveChild($prev) | Out-Null + } + $contentNode.RemoveChild($item) | Out-Null + $modified = $true + } + Write-Host "[OK] Removed from subsystem '$ssName'" + $script:subsystemsCleaned++ + } + } + + if ($modified -and -not $DryRun) { + $enc = New-Object System.Text.UTF8Encoding $true + $sw = New-Object System.IO.StreamWriter($xmlFile.FullName, $false, $enc) + $ssDoc.Save($sw) + $sw.Close() + } + + # Recurse into child subsystems + $childDir = Join-Path $dir ($xmlFile.BaseName) + $childSubsystems = Join-Path $childDir "Subsystems" + if (Test-Path $childSubsystems -PathType Container) { + Remove-FromSubsystems -dir $childSubsystems + } + } +} + +if (Test-Path $subsystemsDir -PathType Container) { + Remove-FromSubsystems -dir $subsystemsDir + if ($subsystemsCleaned -eq 0) { + Write-Host "[OK] Not referenced in any subsystem" + } +} else { + Write-Host "[OK] No Subsystems directory" +} + +# --- 4. Delete object files --- + +Write-Host "" +Write-Host "--- Files ---" + +if (-not $KeepFiles) { + if ($hasDir -and -not $DryRun) { + Remove-Item $objDir -Recurse -Force + Write-Host "[OK] Deleted directory: $typePlural/$objName/" + $actions++ + } elseif ($hasDir) { + Write-Host "[DRY] Would delete directory: $typePlural/$objName/" + $actions++ + } + + if ($hasXml -and -not $DryRun) { + Remove-Item $objXml -Force + Write-Host "[OK] Deleted file: $typePlural/$objName.xml" + $actions++ + } elseif ($hasXml) { + Write-Host "[DRY] Would delete file: $typePlural/$objName.xml" + $actions++ + } + + if (-not $hasXml -and -not $hasDir) { + Write-Host "[OK] No files to delete" + } +} else { + Write-Host "[SKIP] File deletion skipped (-KeepFiles)" +} + +# --- Summary --- + +Write-Host "" +$totalActions = $actions + $subsystemsCleaned +if ($DryRun) { + Write-Host "=== Dry run complete: $totalActions actions would be performed ===" +} else { + Write-Host "=== Done: $totalActions actions performed ($subsystemsCleaned subsystem references removed) ===" +} + +if ($errors -gt 0) { + exit 1 +} +exit 0