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..a531fff8 --- /dev/null +++ b/.claude/skills/meta-remove/SKILL.md @@ -0,0 +1,136 @@ +--- +name: meta-remove +description: Удалить объект метаданных из конфигурации 1С. Используй когда пользователь просит удалить, убрать объект из конфигурации +argument-hint: -Object +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /meta-remove — удаление объекта метаданных + +Безопасно удаляет объект из XML-выгрузки конфигурации. Перед удалением проверяет ссылки на объект в реквизитах, коде и других метаданных. Если ссылки найдены — удаление блокируется. + +## Использование + +``` +/meta-remove -Object +``` + +## Параметры + +| Параметр | Обязательный | Описание | +|------------|:------------:|-------------------------------------------------| +| ConfigDir | да | Корневая директория выгрузки (где Configuration.xml) | +| Object | да | Тип и имя объекта: `Catalog.Товары`, `Document.Заказ` и т.д. | +| DryRun | нет | Только показать что будет удалено, без изменений | +| KeepFiles | нет | Не удалять файлы, только дерегистрировать | +| Force | нет | Удалить несмотря на найденные ссылки | + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\meta-remove\scripts\meta-remove.ps1 -ConfigDir "<путь>" -Object "Catalog.Товары" +``` + +## Что делает + +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.Устаревший === + +[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 'Справочники' + +--- Files --- +[OK] Deleted directory: Catalogs/Устаревший/ +[OK] Deleted file: Catalogs/Устаревший.xml + +=== Done: 4 actions performed (1 subsystem references removed) === +``` + +## Вывод (объект со ссылками — блокировка) + +``` +=== meta-remove: Catalog.Валюты === + +[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 +... -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 + +# Удалить общий модуль +... -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..10b683d6 --- /dev/null +++ b/.claude/skills/meta-remove/scripts/meta-remove.ps1 @@ -0,0 +1,475 @@ +# meta-remove v1.1 — 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, + + [switch]$Force +) + +$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. 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 } + } + # 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)) { + $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 ---" + +$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" + } +} + +# --- 4. 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" +} + +# --- 5. 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 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/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` ## Сценарии использования 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 и подсистем. Если есть ссылки — покажет список и заблокирует удаление. + ### Проверка объекта после изменений ``` 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, элементы