From 151ded9baee93cad133a3964eb3fca0e5792132c Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 16 Feb 2026 16:35:23 +0300 Subject: [PATCH 1/6] feat(skills): add epf-validate, erf-validate and meta-remove skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - epf-validate/erf-validate: 10-check structural validator for EPF/ERF XML sources (root structure, InternalInfo/ClassId, properties, ChildObjects types/ordering, cross-references, attributes, tabular sections, name uniqueness, file existence, form descriptors). Single PS1 script auto-detects EPF vs ERF. - meta-remove: delete metadata objects from config XML dump — removes files, deregisters from Configuration.xml ChildObjects, recursively cleans subsystem Content references. Supports -DryRun and -KeepFiles. - db-list: updated resolution algorithm with glob pattern support for branch matching and post-execution registration offer. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/db-list/SKILL.md | 24 +- .claude/skills/epf-validate/SKILL.md | 88 ++ .../epf-validate/scripts/epf-validate.ps1 | 824 ++++++++++++++++++ .claude/skills/erf-validate/SKILL.md | 88 ++ .claude/skills/meta-remove/SKILL.md | 100 +++ .../meta-remove/scripts/meta-remove.ps1 | 307 +++++++ 6 files changed, 1421 insertions(+), 10 deletions(-) create mode 100644 .claude/skills/epf-validate/SKILL.md create mode 100644 .claude/skills/epf-validate/scripts/epf-validate.ps1 create mode 100644 .claude/skills/erf-validate/SKILL.md create mode 100644 .claude/skills/meta-remove/SKILL.md create mode 100644 .claude/skills/meta-remove/scripts/meta-remove.ps1 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 From 27bb08d1276b2ccb58f3a329be6e8dd6e4385b4f Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 16 Feb 2026 16:55:22 +0300 Subject: [PATCH 2/6] fix(meta-remove): add reference check before deletion and fix PS 5.1 variable interpolation Block deletion when object has references in attributes, code, or forms. Add -Force parameter to override. Exclude ConfigDumpInfo.xml from ref check. Fix ${objType}.${objName} syntax for PS 5.1 compatibility. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/meta-remove/SKILL.md | 68 +++++-- .../meta-remove/scripts/meta-remove.ps1 | 180 +++++++++++++++++- 2 files changed, 225 insertions(+), 23 deletions(-) diff --git a/.claude/skills/meta-remove/SKILL.md b/.claude/skills/meta-remove/SKILL.md index 0196387c..a531fff8 100644 --- a/.claude/skills/meta-remove/SKILL.md +++ b/.claude/skills/meta-remove/SKILL.md @@ -11,7 +11,7 @@ allowed-tools: # /meta-remove — удаление объекта метаданных -Удаляет объект из XML-выгрузки конфигурации: файлы, регистрацию в Configuration.xml, ссылки в подсистемах. +Безопасно удаляет объект из XML-выгрузки конфигурации. Перед удалением проверяет ссылки на объект в реквизитах, коде и других метаданных. Если ссылки найдены — удаление блокируется. ## Использование @@ -27,6 +27,7 @@ allowed-tools: | Object | да | Тип и имя объекта: `Catalog.Товары`, `Document.Заказ` и т.д. | | DryRun | нет | Только показать что будет удалено, без изменений | | KeepFiles | нет | Не удалять файлы, только дерегистрировать | +| Force | нет | Удалить несмотря на найденные ссылки | ## Команда @@ -36,16 +37,20 @@ powershell.exe -NoProfile -File .claude\skills\meta-remove\scripts\meta-remove.p ## Что делает -1. **Находит файлы объекта**: `{TypePlural}/{Name}.xml` и `{TypePlural}/{Name}/` (каталог с модулями, формами, макетами) -2. **Удаляет из Configuration.xml**: убирает `Name` из `` -3. **Очищает подсистемы**: рекурсивно обходит все `Subsystems/` и удаляет `Type.Name` из `` -4. **Удаляет файлы**: XML-файл и каталог объекта +1. **Находит файлы объекта**: `{TypePlural}/{Name}.xml` и `{TypePlural}/{Name}/` +2. **Проверяет ссылки** (блокирует при наличии, если нет `-Force`): + - XML-типы в реквизитах других объектов: `CatalogRef.Имя`, `DocumentRef.Имя` и т.д. + - BSL-код: `Справочники.Имя`, `Catalogs.Имя`, вызовы общих модулей + - Журналы документов, подписки на события, определяемые типы +3. **Удаляет из Configuration.xml**: убирает из `` +4. **Очищает подсистемы**: рекурсивно удаляет из `` +5. **Удаляет файлы**: 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.Устаревший === @@ -53,39 +58,70 @@ Catalog, Document, Enum, Constant, InformationRegister, AccumulationRegister, Ac [FOUND] Catalogs/Устаревший.xml [FOUND] Catalogs/Устаревший/ (8 files) +--- Reference check --- +[OK] No references found + --- 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) === +=== Done: 4 actions performed (1 subsystem references removed) === ``` -Код возврата: 0 = успешно, 1 = ошибки. +## Вывод (объект со ссылками — блокировка) -## Безопасность +``` +=== meta-remove: Catalog.Валюты === -- **Рекомендуется**: сначала запустить с `-DryRun` для проверки -- **ВАЖНО**: перед удалением убедитесь что на объект нет ссылок в коде (типы реквизитов, запросы, вызовы модулей) -- Скрипт НЕ проверяет ссылки из кода — только структурные (Configuration.xml, подсистемы) -- Для проверки ссылок из кода используйте поиск по конфигурации: `grep -r "Type.Name" ` +[FOUND] Catalogs/Валюты.xml +[FOUND] Catalogs/Валюты/ (4 files) + +--- Reference check --- +[WARN] Found 3 reference(s) to Catalog.Валюты: + + Documents/СчетНаОплату.xml + pattern: CatalogRef.Валюты + InformationRegisters/КурсыВалют.xml + pattern: CatalogRef.Валюты + CommonModules/РаботаСВалютами/Ext/Module.bsl + pattern: Справочники.Валюты + +[ERROR] Cannot remove: object has 3 reference(s). + Use -Force to remove anyway, or fix references first. +``` + +Код возврата: 0 = успешно, 1 = ошибки или найдены ссылки. + +## Проверяемые ссылки + +| Категория | Паттерны поиска | +|-----------|----------------| +| XML-типы реквизитов | `CatalogRef.Name`, `DocumentRef.Name`, `EnumRef.Name` и др. | +| BSL-код (рус.) | `Справочники.Name`, `Документы.Name`, `Перечисления.Name` и др. | +| BSL-код (англ.) | `Catalogs.Name`, `Documents.Name`, `Enums.Name` и др. | +| Общие модули | `Name.` (вызовы методов), `Name.`, `Name.` | + +Ссылки из Configuration.xml, ConfigDumpInfo.xml и подсистем НЕ считаются блокирующими — они очищаются автоматически. ## Примеры ```powershell -# Dry run — посмотреть что будет удалено +# Проверка ссылок + 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 "Catalog.Устаревший" -Force + # Только дерегистрировать (файлы оставить) ... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Report.Старый" -KeepFiles diff --git a/.claude/skills/meta-remove/scripts/meta-remove.ps1 b/.claude/skills/meta-remove/scripts/meta-remove.ps1 index 193b0c5e..84ec96fd 100644 --- a/.claude/skills/meta-remove/scripts/meta-remove.ps1 +++ b/.claude/skills/meta-remove/scripts/meta-remove.ps1 @@ -1,4 +1,4 @@ -# meta-remove v1.0 — Remove metadata object from 1C configuration dump +# meta-remove v1.1 — Remove metadata object from 1C configuration dump # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -9,7 +9,9 @@ param( [switch]$DryRun, - [switch]$KeepFiles + [switch]$KeepFiles, + + [switch]$Force ) $ErrorActionPreference = "Stop" @@ -94,7 +96,7 @@ if (-not $typePluralMap.ContainsKey($objType)) { $typePlural = $typePluralMap[$objType] -Write-Host "=== meta-remove: $objType.$objName ===" +Write-Host "=== meta-remove: ${objType}.${objName} ===" Write-Host "" if ($DryRun) { @@ -125,7 +127,171 @@ if (-not $hasXml -and -not $hasDir) { } } -# --- 2. Remove from Configuration.xml ChildObjects --- +# --- 2. Reference check --- + +Write-Host "" +Write-Host "--- Reference check ---" + +# Build search patterns based on object type + +# Type → reference type name (used in XML elements) +$typeRefNames = @{ + "Catalog" = @("CatalogRef","CatalogObject") + "Document" = @("DocumentRef","DocumentObject") + "Enum" = @("EnumRef") + "ExchangePlan" = @("ExchangePlanRef","ExchangePlanObject") + "ChartOfAccounts" = @("ChartOfAccountsRef","ChartOfAccountsObject") + "ChartOfCharacteristicTypes" = @("ChartOfCharacteristicTypesRef","ChartOfCharacteristicTypesObject") + "ChartOfCalculationTypes" = @("ChartOfCalculationTypesRef","ChartOfCalculationTypesObject") + "BusinessProcess" = @("BusinessProcessRef","BusinessProcessObject") + "Task" = @("TaskRef","TaskObject") +} + +# Type → Russian manager name (used in BSL code: Справочники.Товары) +$typeRuManager = @{ + "Catalog" = "Справочники" + "Document" = "Документы" + "Enum" = "Перечисления" + "Constant" = "Константы" + "InformationRegister" = "РегистрыСведений" + "AccumulationRegister" = "РегистрыНакопления" + "AccountingRegister" = "РегистрыБухгалтерии" + "CalculationRegister" = "РегистрыРасчета" + "ChartOfAccounts" = "ПланыСчетов" + "ChartOfCharacteristicTypes" = "ПланыВидовХарактеристик" + "ChartOfCalculationTypes" = "ПланыВидовРасчета" + "BusinessProcess" = "БизнесПроцессы" + "Task" = "Задачи" + "ExchangePlan" = "ПланыОбмена" + "Report" = "Отчеты" + "DataProcessor" = "Обработки" + "DocumentJournal" = "ЖурналыДокументов" + "CommonModule" = $null +} + +$searchPatterns = @() + +# 1) XML type references: CatalogRef.Name, CatalogObject.Name +if ($typeRefNames.ContainsKey($objType)) { + foreach ($refName in $typeRefNames[$objType]) { + $searchPatterns += "$refName.$objName" + } +} + +# 2) BSL code references: Справочники.Name, Catalogs.Name +$ruMgr = $typeRuManager[$objType] +if ($ruMgr) { + $searchPatterns += "$ruMgr.$objName" +} +# English manager = plural directory name +$searchPatterns += "$typePlural.$objName" + +# 3) CommonModule: method calls in BSL (ModuleName.) +if ($objType -eq "CommonModule") { + $searchPatterns += "$objName." +} + +# 4) ScheduledJob/EventSubscription handler references +if ($objType -eq "CommonModule") { + $searchPatterns += "$objName." + $searchPatterns += "$objName." +} + +# Exclude object's own files from search +$excludeDirs = @() +if ($hasDir) { $excludeDirs += $objDir } +$excludeFile = "" +if ($hasXml) { $excludeFile = $objXml } + +# Search all XML and BSL files +$references = @() +$searchExtensions = @("*.xml", "*.bsl") + +foreach ($ext in $searchExtensions) { + $files = @(Get-ChildItem $ConfigDir -Filter $ext -Recurse -File -ErrorAction SilentlyContinue) + foreach ($file in $files) { + # Skip own files + if ($excludeFile -and $file.FullName -eq $excludeFile) { continue } + if ($excludeDirs.Count -gt 0) { + $skip = $false + foreach ($ed in $excludeDirs) { + if ($file.FullName.StartsWith($ed)) { $skip = $true; break } + } + if ($skip) { continue } + } + + $content = [System.IO.File]::ReadAllText($file.FullName, [System.Text.Encoding]::UTF8) + foreach ($pat in $searchPatterns) { + if ($content.Contains($pat)) { + $relPath = $file.FullName.Substring($ConfigDir.Length + 1) + $references += @{ File = $relPath; Pattern = $pat } + break # one match per file is enough + } + } + } +} + +# Also check for Type.Name references (subsystem content, doc journal, etc.) — but NOT in own files +$typeNameRef = "${objType}.${objName}" +$files = @(Get-ChildItem $ConfigDir -Filter "*.xml" -Recurse -File -ErrorAction SilentlyContinue) +foreach ($file in $files) { + if ($excludeFile -and $file.FullName -eq $excludeFile) { continue } + if ($excludeDirs.Count -gt 0) { + $skip = $false + foreach ($ed in $excludeDirs) { + if ($file.FullName.StartsWith($ed)) { $skip = $true; break } + } + if ($skip) { continue } + } + # Skip Configuration.xml and Subsystems — they will be cleaned automatically + $relPath = $file.FullName.Substring($ConfigDir.Length + 1) + if ($relPath -eq "Configuration.xml") { continue } + if ($relPath -eq "ConfigDumpInfo.xml") { continue } + if ($relPath.StartsWith("Subsystems")) { continue } + + $content = [System.IO.File]::ReadAllText($file.FullName, [System.Text.Encoding]::UTF8) + if ($content.Contains($typeNameRef)) { + # Check it's not already in references + $alreadyFound = $false + foreach ($r in $references) { + if ($r.File -eq $relPath) { $alreadyFound = $true; break } + } + if (-not $alreadyFound) { + $references += @{ File = $relPath; Pattern = $typeNameRef } + } + } +} + +if ($references.Count -gt 0) { + Write-Host "[WARN] Found $($references.Count) reference(s) to ${objType}.${objName}:" + Write-Host "" + $shown = 0 + foreach ($ref in $references) { + Write-Host " $($ref.File)" + Write-Host " pattern: $($ref.Pattern)" + $shown++ + if ($shown -ge 20) { + $remaining = $references.Count - $shown + if ($remaining -gt 0) { + Write-Host " ... and $remaining more" + } + break + } + } + Write-Host "" + + if (-not $Force) { + Write-Host "[ERROR] Cannot remove: object has $($references.Count) reference(s)." + Write-Host " Use -Force to remove anyway, or fix references first." + exit 1 + } else { + Write-Host "[WARN] -Force specified, proceeding despite references" + } +} else { + Write-Host "[OK] No references found" +} + +# --- 3. Remove from Configuration.xml ChildObjects --- Write-Host "" Write-Host "--- Configuration.xml ---" @@ -178,7 +344,7 @@ if (-not $cfgNode) { } } -# --- 3. Remove from subsystem Content --- +# --- 4. Remove from subsystem Content --- Write-Host "" Write-Host "--- Subsystems ---" @@ -213,7 +379,7 @@ function Remove-FromSubsystems { $ssName = if ($ssNameNode) { $ssNameNode.InnerText } else { $xmlFile.BaseName } # Content items are Type.Name - $targetRef = "$objType.$objName" + $targetRef = "${objType}.${objName}" $modified = $false foreach ($item in @($contentNode.ChildNodes)) { @@ -260,7 +426,7 @@ if (Test-Path $subsystemsDir -PathType Container) { Write-Host "[OK] No Subsystems directory" } -# --- 4. Delete object files --- +# --- 5. Delete object files --- Write-Host "" Write-Host "--- Files ---" From d7f012b307f6cfe28ebfbd1a91cf85f0aa679c15 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 16 Feb 2026 17:04:55 +0300 Subject: [PATCH 3/6] fix(meta-remove): exclude Configuration.xml, ConfigDumpInfo.xml and Subsystems from both search loops Co-Authored-By: Claude Opus 4.6 --- .claude/skills/meta-remove/scripts/meta-remove.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.claude/skills/meta-remove/scripts/meta-remove.ps1 b/.claude/skills/meta-remove/scripts/meta-remove.ps1 index 84ec96fd..10b683d6 100644 --- a/.claude/skills/meta-remove/scripts/meta-remove.ps1 +++ b/.claude/skills/meta-remove/scripts/meta-remove.ps1 @@ -219,11 +219,13 @@ foreach ($ext in $searchExtensions) { } if ($skip) { continue } } + # Skip auto-cleaned files (Configuration.xml, ConfigDumpInfo.xml, Subsystems) + $relPath = $file.FullName.Substring($ConfigDir.Length + 1) + if ($relPath -eq "Configuration.xml" -or $relPath -eq "ConfigDumpInfo.xml" -or $relPath.StartsWith("Subsystems")) { continue } $content = [System.IO.File]::ReadAllText($file.FullName, [System.Text.Encoding]::UTF8) foreach ($pat in $searchPatterns) { if ($content.Contains($pat)) { - $relPath = $file.FullName.Substring($ConfigDir.Length + 1) $references += @{ File = $relPath; Pattern = $pat } break # one match per file is enough } From cfcc6332571f0653f1be89fe72ed2a34d951682f Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 16 Feb 2026 17:12:25 +0300 Subject: [PATCH 4/6] docs: update README and guides with epf-validate, erf-validate, meta-remove Co-Authored-By: Claude Opus 4.6 --- README.md | 9 ++++++--- docs/epf-guide.md | 2 ++ docs/meta-guide.md | 15 +++++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 58585ad4..56eaf23d 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,14 @@ | Группа | Навыки | Описание | Гайд | |--------|--------|----------|------| -| Внешние обработки (EPF) | 6 навыков `/epf-*` | Создание, сборка, разборка обработок из XML-исходников | [Подробнее](docs/epf-guide.md) | -| Внешние отчёты (ERF) | 3 навыка `/erf-*` | Создание, сборка, разборка внешних отчётов | [Подробнее](docs/epf-guide.md#внешние-отчёты-erf) | +| Внешние обработки (EPF) | 7 навыков `/epf-*` | Создание, сборка, разборка, валидация обработок из XML-исходников | [Подробнее](docs/epf-guide.md) | +| Внешние отчёты (ERF) | 4 навыка `/erf-*` | Создание, сборка, разборка, валидация внешних отчётов | [Подробнее](docs/epf-guide.md#внешние-отчёты-erf) | | Универсальные операции | `/template-add`, `/template-remove`, `/help-add`, `/form-remove` | Добавление/удаление макетов, форм, справки для любых объектов | [Подробнее](docs/epf-guide.md#универсальные-навыки) | | Табличный документ (MXL) | 4 навыка `/mxl-*` | Анализ, создание, компиляция макетов печатных форм | [Подробнее](docs/mxl-guide.md) | | Управляемые формы (Form) | 6 навыков `/form-*` | Создание, анализ, генерация, модификация, валидация управляемых форм | [Подробнее](docs/form-guide.md) | | Роли (Role) | 3 навыка `/role-*` | Анализ прав роли, создание из JSON DSL, валидация | [Подробнее](docs/role-guide.md) | | Схема компоновки (СКД) | 4 навыка `/skd-*` | Анализ, генерация из JSON DSL, точечное редактирование, валидация схем компоновки данных | [Подробнее](docs/skd-guide.md) | -| Метаданные конфигурации | 4 навыка `/meta-*` | Создание, анализ, редактирование, валидация объектов метаданных (23 типа) | [Подробнее](docs/meta-guide.md) | +| Метаданные конфигурации | 5 навыков `/meta-*` | Создание, анализ, редактирование, удаление, валидация объектов метаданных (23 типа) | [Подробнее](docs/meta-guide.md) | | Корневая конфигурация | 4 навыка `/cf-*` | Создание, анализ, редактирование, валидация корневых файлов конфигурации | [Подробнее](docs/cf-guide.md) | | Расширения (CFE) | 5 навыков `/cfe-*` | Создание, заимствование, перехват методов, валидация, анализ расширений | [Подробнее](docs/cfe-guide.md) | | Подсистемы (Subsystem) | 4 навыка `/subsystem-*` | Анализ, создание, редактирование, валидация подсистем конфигурации | — | @@ -70,9 +70,11 @@ ├── epf-dump/ # Разборка EPF ├── epf-bsp-init/ # Регистрация БСП ├── epf-bsp-add-command/ # Команда БСП +├── epf-validate/ # Валидация обработки ├── erf-init/ # Создание внешнего отчёта ├── erf-build/ # Сборка ERF ├── erf-dump/ # Разборка ERF +├── erf-validate/ # Валидация отчёта ├── template-add/ # Добавление макета (универсальный) ├── template-remove/ # Удаление макета (универсальный) ├── form-add/ # Добавление формы (универсальный) @@ -97,6 +99,7 @@ ├── meta-info/ # Структура объекта метаданных ├── meta-compile/ # Создание объекта метаданных ├── meta-edit/ # Редактирование объекта метаданных +├── meta-remove/ # Удаление объекта метаданных ├── meta-validate/ # Валидация объекта метаданных ├── cf-info/ # Анализ структуры конфигурации ├── cf-init/ # Создание пустой конфигурации diff --git a/docs/epf-guide.md b/docs/epf-guide.md index d59d4c66..e58e3584 100644 --- a/docs/epf-guide.md +++ b/docs/epf-guide.md @@ -12,6 +12,7 @@ | `/epf-dump` | `` | Разобрать EPF в XML (через 1cv8.exe) | | `/epf-bsp-init` | ` <Вид>` | Добавить регистрацию БСП (СведенияОВнешнейОбработке) | | `/epf-bsp-add-command` | ` <Идентификатор>` | Добавить команду в дополнительную обработку БСП | +| `/epf-validate` | ` [-MaxErrors 30]` | Валидация структурной корректности обработки (10 проверок) | ## Внешние отчёты (ERF) @@ -20,6 +21,7 @@ | `/erf-init` | ` [Synonym] [--WithSKD]` | Создать новый отчёт (корневой XML + модуль объекта + опционально СКД) | | `/erf-build` | `` | Собрать ERF из XML (через 1cv8.exe) | | `/erf-dump` | `` | Разобрать ERF в XML (через 1cv8.exe) | +| `/erf-validate` | ` [-MaxErrors 30]` | Валидация структурной корректности отчёта (10 проверок) | Флаг `--WithSKD` создаёт макет `ОсновнаяСхемаКомпоновкиДанных` и привязывает его к `MainDataCompositionSchema`. diff --git a/docs/meta-guide.md b/docs/meta-guide.md index 42b64d83..a5880d39 100644 --- a/docs/meta-guide.md +++ b/docs/meta-guide.md @@ -9,6 +9,7 @@ | `/meta-info` | ` [-Mode] [-Name]` | Анализ структуры объекта: реквизиты, ТЧ, формы, движения, типы (8 режимов) | | `/meta-compile` | ` ` | Создание объекта метаданных из JSON DSL: реквизиты, ТЧ, свойства, формы | | `/meta-edit` | ` -Operation -Value ""` | Точечное редактирование: 30+ атомарных операций (add/remove/modify/set) | +| `/meta-remove` | ` -Object [-DryRun] [-Force]` | Безопасное удаление объекта с проверкой ссылок (блокирует при наличии) | | `/meta-validate` | ` [-MaxErrors 20]` | Валидация структурной корректности: ~40 проверок | ## Рабочий цикл @@ -16,13 +17,15 @@ ``` Описание объекта (текст) → JSON DSL → /meta-compile → XML-исходники → /meta-validate ↕ /meta-edit → /meta-info + ↕ /meta-remove (безопасное удаление) ``` 1. Claude формирует JSON-определение объекта (тип, реквизиты, ТЧ, свойства) 2. `/meta-compile` генерирует XML-исходники с корректными UUID, namespace, структурой ChildObjects 3. `/meta-edit` вносит точечные изменения: добавление/удаление реквизитов, ТЧ, владельцев, движений и т.д. -4. `/meta-validate` проверяет корректность XML -5. `/meta-info` выводит компактную сводку для визуальной проверки +4. `/meta-remove` безопасно удаляет объект (проверяет ссылки в реквизитах, коде, формах; чистит Configuration.xml и подсистемы) +5. `/meta-validate` проверяет корректность XML +6. `/meta-info` выводит компактную сводку для визуальной проверки ## Поддерживаемые типы объектов (23 типа) @@ -147,6 +150,14 @@ Claude вызовет `/meta-edit` дважды: `add-attribute` для рекв Claude вызовет `/meta-edit` с операцией `set-registerRecords`. +### Удаление неиспользуемого объекта + +``` +> Удали справочник Catalogs/Устаревший из конфигурации src/ +``` + +Claude вызовет `/meta-remove` с `-DryRun`, покажет что будет удалено и проверит ссылки. Если объект нигде не используется — удалит файлы, уберёт из Configuration.xml и подсистем. Если есть ссылки — покажет список и заблокирует удаление. + ### Проверка объекта после изменений ``` From 4b0304d0c866dda1c7387a6b3f58d882ffcb6b68 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 16 Feb 2026 17:14:36 +0300 Subject: [PATCH 5/6] docs(db-guide): update resolution algorithm, add glob patterns for branches Co-Authored-By: Claude Opus 4.6 --- docs/db-guide.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/db-guide.md b/docs/db-guide.md index 13f59b89..459cdf44 100644 --- a/docs/db-guide.md +++ b/docs/db-guide.md @@ -51,7 +51,7 @@ "user": "Admin", "password": "", "aliases": ["dev", "разработка"], - "branches": ["dev", "develop"], + "branches": ["dev", "develop", "feature/*"], "configSrc": "C:\\WS\\myapp\\cfsrc" }, { @@ -82,12 +82,19 @@ | `user` | string | нет | Пользователь 1С | | `password` | string | нет | Пароль | | `aliases` | string[] | нет | Альтернативные имена | -| `branches` | string[] | нет | Git-ветки | +| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`) | | `configSrc` | string | нет | Каталог XML-выгрузки | ### Разрешение базы -Все навыки `db-*` используют единый алгоритм: id → alias → branch → name → default → спросить пользователя. +Все навыки `db-*` (а также `epf-build`, `epf-dump`, `erf-build`, `erf-dump`) используют единый алгоритм: + +1. Если пользователь указал **параметры подключения** (путь, сервер) — используй напрямую +2. Если указал **базу по имени** — ищи: `id` → `aliases` (с учётом морфологии) → `name` (нечёткое) +3. Если **не указал** — сопоставь текущую ветку Git с `branches` (точно или по glob-паттерну) +4. Fallback на `default` +5. Если не найдено — спроси пользователя +6. После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add` ## Сценарии использования From 1364f22df325960720b7cdc42d22aa3d0239184a Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 16 Feb 2026 17:35:18 +0300 Subject: [PATCH 6/6] docs: add subsystem and command interface guide Co-Authored-By: Claude Opus 4.6 --- docs/subsystem-guide.md | 205 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 docs/subsystem-guide.md diff --git a/docs/subsystem-guide.md b/docs/subsystem-guide.md new file mode 100644 index 00000000..850cdd1c --- /dev/null +++ b/docs/subsystem-guide.md @@ -0,0 +1,205 @@ +# Подсистемы и командный интерфейс + +Навыки групп `/subsystem-*` и `/interface-*` позволяют анализировать, создавать, редактировать и проверять подсистемы 1С и их командный интерфейс — XML-файлы Subsystem и CommandInterface.xml из выгрузки конфигурации. + +## Навыки + +| Навык | Параметры | Описание | +|-------|-----------|----------| +| `/subsystem-info` | ` [-Mode] [-Name]` | Анализ структуры подсистемы: состав, дочерние, CI, дерево иерархии (4 режима) | +| `/subsystem-compile` | ` [-Parent]` | Генерация подсистемы из JSON DSL: XML + регистрация в Configuration.xml | +| `/subsystem-edit` | ` -Operation -Value ""` | Точечное редактирование: 5 операций (add/remove content/child, set-property) | +| `/subsystem-validate` | ` [-MaxErrors 30]` | Валидация структурной корректности: 13 проверок | +| `/interface-edit` | ` -Operation -Value ""` | Настройка CommandInterface.xml: 6 операций (hide/show/place/order) | +| `/interface-validate` | ` [-MaxErrors 30]` | Валидация CommandInterface.xml: 13 проверок | + +## Рабочий цикл + +``` +Описание раздела (текст) → JSON DSL → /subsystem-compile → XML → /subsystem-validate + ↕ /subsystem-edit → /subsystem-info + /interface-edit → CommandInterface.xml → /interface-validate +``` + +1. Claude формирует JSON-определение подсистемы (имя, состав, дочерние, картинка) +2. `/subsystem-compile` генерирует XML-файл подсистемы и регистрирует в Configuration.xml +3. `/subsystem-edit` вносит точечные изменения: добавление объектов, дочерних подсистем, изменение свойств +4. `/interface-edit` настраивает командный интерфейс: скрытие/показ команд, размещение в группах, порядок +5. `/subsystem-validate` и `/interface-validate` проверяют корректность XML +6. `/subsystem-info` выводит компактную сводку для визуальной проверки + +## JSON DSL — формат подсистемы + +Подсистемы описываются в JSON: + +### Минимальный пример + +```json +{ + "name": "Тест" +} +``` + +Умолчания: `includeInCommandInterface = true`, `useOneCommand = false`, synonym генерируется из name. + +### Полный пример + +```json +{ + "name": "Продажи", + "synonym": "Продажи", + "comment": "", + "includeInCommandInterface": true, + "useOneCommand": false, + "explanation": "Управление продажами и взаимодействием с клиентами", + "picture": "CommonPicture.Продажи", + "content": ["Catalog.Товары", "Document.Заказ", "Report.Продажи"], + "children": ["Настройки", "Отчёты"] +} +``` + +### Свойства + +| Свойство | Тип | Умолчание | Описание | +|----------|-----|-----------|----------| +| `name` | string | *(обязательно)* | Идентификатор подсистемы | +| `synonym` | string | из name | Отображаемое имя | +| `comment` | string | `""` | Комментарий | +| `includeInCommandInterface` | bool | `true` | Включать в командный интерфейс | +| `useOneCommand` | bool | `false` | Режим одной команды (требует ровно 1 элемент в content) | +| `explanation` | string | `""` | Описание раздела (подсказка) | +| `picture` | string | — | Ссылка на общую картинку (`CommonPicture.Имя`) | +| `content` | string[] | `[]` | Состав: `"Тип.Имя"` (Catalog.X, Document.Y, Report.Z, ...) | +| `children` | string[] | `[]` | Имена дочерних подсистем | + +## Командный интерфейс — операции + +Навык `/interface-edit` управляет файлом `CommandInterface.xml` подсистемы. + +### Форматы ссылок на команды + +| Формат | Пример | +|--------|--------| +| StandardCommand | `Catalog.Товары.StandardCommand.OpenList` | +| Command | `Report.Продажи.Command.Отчёт` | +| CommonCommand | `CommonCommand.МояКоманда` | + +### Операции + +| Операция | Значение | Описание | +|----------|----------|----------| +| `hide` | `"Cmd.Name"` или массив | Скрыть команду (CommandsVisibility, false) | +| `show` | `"Cmd.Name"` или массив | Показать команду (CommandsVisibility, true) | +| `place` | `{"command":"...","group":"CommandGroup.X"}` | Разместить команду в группе | +| `order` | `{"group":"...","commands":[...]}` | Задать порядок команд в группе | +| `subsystem-order` | `["Subsystem.X.Subsystem.A",...]` | Порядок дочерних подсистем | +| `group-order` | `["NavigationPanelOrdinary",...]` | Порядок групп | + +### Примеры + +```powershell +# Скрыть команду +... -CIPath Subsystems/Продажи/Ext/CommandInterface.xml -Operation hide -Value "Catalog.Товары.StandardCommand.OpenList" + +# Показать команду +... -Operation show -Value "Report.Продажи.Command.Отчёт" + +# Разместить в группе +... -Operation place -Value '{"command":"Report.X.Command.Y","group":"CommandGroup.Отчеты"}' + +# Задать порядок подсистем +... -Operation subsystem-order -Value '["Subsystem.X.Subsystem.A","Subsystem.X.Subsystem.B"]' +``` + +## Сценарии использования + +### Анализ структуры подсистем + +``` +> Покажи дерево подсистем конфигурации +``` + +Claude вызовет `/subsystem-info` (tree → overview → ci) и опишет: +- иерархию подсистем с маркерами [CI], [OneCmd], [Скрыт] +- состав каждой подсистемы (объекты по типам) +- настройки командного интерфейса (видимость, размещение, порядок) + +### Создание подсистемы по описанию + +``` +> Создай подсистему Продажи: справочник Товары, документ Заказ, отчёт Продажи. +> Дочерние подсистемы: Настройки, Отчёты. Картинка — CommonPicture.Продажи. +``` + +Claude сформирует JSON: +```json +{ + "name": "Продажи", + "synonym": "Продажи", + "content": ["Catalog.Товары", "Document.Заказ", "Report.Продажи"], + "children": ["Настройки", "Отчёты"], + "picture": "CommonPicture.Продажи" +} +``` + +И вызовет `/subsystem-compile` → `/subsystem-validate` → `/subsystem-info`. + +### Добавление объектов в подсистему + +``` +> Добавь Document.Счёт и Report.Задолженность в подсистему Продажи +``` + +Claude вызовет `/subsystem-edit` с операцией `add-content`: +```powershell +... -SubsystemPath Subsystems/Продажи.xml -Operation add-content -Value '["Document.Счёт","Report.Задолженность"]' +``` + +### Настройка командного интерфейса + +``` +> Скрой команду открытия списка товаров и размести отчёт Продажи в группе Отчёты +``` + +Claude вызовет `/interface-edit`: +```powershell +# Скрыть команду +... -CIPath Subsystems/Продажи/Ext/CommandInterface.xml -Operation hide -Value "Catalog.Товары.StandardCommand.OpenList" + +# Разместить в группе +... -Operation place -Value '{"command":"Report.Продажи.Command.Отчёт","group":"CommandGroup.Отчёты"}' +``` + +### Проверка подсистемы + +``` +> Проверь подсистему Subsystems/Продажи.xml +``` + +Claude вызовет `/subsystem-validate` и `/interface-validate`, покажет результат: ошибки (невалидный XML, отсутствующие файлы, дубликаты) и предупреждения. + +## Структура файлов подсистемы + +``` +Subsystems/ +├── ИмяПодсистемы.xml # Определение подсистемы (UUID, свойства, Content) +└── ИмяПодсистемы/ + ├── Ext/ + │ └── CommandInterface.xml # Командный интерфейс (видимость, размещение, порядок) + └── Subsystems/ # Вложенные подсистемы + ├── Дочерняя.xml + └── Дочерняя/ + └── Ext/ + └── CommandInterface.xml +``` + +Регистрация в `Configuration.xml`: +```xml + + ИмяПодсистемы + +``` + +## Спецификация + +- [1c-subsystem-spec.md](1c-subsystem-spec.md) — XML-формат подсистем, CommandInterface.xml, namespace, элементы