feat(skills): add epf-validate, erf-validate and meta-remove skills

- 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 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-02-16 16:35:23 +03:00
parent 05362aeb96
commit 151ded9bae
6 changed files with 1421 additions and 10 deletions
+14 -10
View File
@@ -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`.
### Автоопределение платформы
+88
View File
@@ -0,0 +1,88 @@
---
name: epf-validate
description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности
argument-hint: <ObjectPath> [-MaxErrors 30]
allowed-tools:
- Bash
- Read
- Glob
---
# /epf-validate — валидация внешней обработки (EPF)
Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов.
Скрипт также работает для внешних отчётов (ERF) — автоопределение по типу элемента. См. `/erf-validate`.
## Использование
```
/epf-validate <ObjectPath>
```
## Параметры
| Параметр | Обязательный | По умолчанию | Описание |
|------------|:------------:|--------------|-------------------------------------------------|
| ObjectPath | да | — | Путь к корневому XML или каталогу обработки |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
`ObjectPath` авторезолв: если указана директория — ищет `<dirName>/<dirName>.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 <Name> — создать обработку
/epf-validate src/<Name>.xml — проверить результат
/epf-build <Name> — собрать EPF
```
## Когда использовать
- **После `/epf-init`**: проверить scaffold
- **После добавления формы/макета**: убедиться что ChildObjects, файлы и ссылки корректны
- **После ручного редактирования XML**: выявить структурные ошибки до сборки
- **При отладке сборки**: найти причину ошибки Designer
@@ -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 <Form> 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
+88
View File
@@ -0,0 +1,88 @@
---
name: erf-validate
description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности
argument-hint: <ObjectPath> [-MaxErrors 30]
allowed-tools:
- Bash
- Read
- Glob
---
# /erf-validate — валидация внешнего отчёта (ERF)
Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов.
Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport).
## Использование
```
/erf-validate <ObjectPath>
```
## Параметры
| Параметр | Обязательный | По умолчанию | Описание |
|------------|:------------:|--------------|-------------------------------------------------|
| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
`ObjectPath` авторезолв: если указана директория — ищет `<dirName>/<dirName>.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 <Name> --with-skd — создать отчёт с СКД
/erf-validate src/<Name>.xml — проверить результат
/erf-build <Name> — собрать ERF
```
## Когда использовать
- **После `/erf-init`**: проверить scaffold
- **После добавления формы/макета/СКД**: убедиться что ChildObjects и MainDCS корректны
- **После ручного редактирования XML**: выявить структурные ошибки до сборки
- **При отладке сборки**: найти причину ошибки Designer
+100
View File
@@ -0,0 +1,100 @@
---
name: meta-remove
description: Удалить объект метаданных из конфигурации 1С. Используй когда пользователь просит удалить, убрать объект из конфигурации
argument-hint: <ConfigDir> -Object <Type.Name>
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /meta-remove — удаление объекта метаданных
Удаляет объект из XML-выгрузки конфигурации: файлы, регистрацию в Configuration.xml, ссылки в подсистемах.
## Использование
```
/meta-remove <ConfigDir> -Object <Type.Name>
```
## Параметры
| Параметр | Обязательный | Описание |
|------------|:------------:|-------------------------------------------------|
| 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**: убирает `<Type>Name</Type>` из `<ChildObjects>`
3. **Очищает подсистемы**: рекурсивно обходит все `Subsystems/` и удаляет `<v8:Value>Type.Name</v8:Value>` из `<Content>`
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 <Catalog>Устаревший</Catalog> 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" <ConfigDir>`
## Примеры
```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.МойМодуль"
```
## Когда использовать
- **Рефакторинг**: удаление неиспользуемых объектов
- **Очистка**: удаление временных/тестовых объектов
- **Перенос**: удаление объекта перед пересозданием с другой структурой
@@ -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</$objType> from ChildObjects"
$actions++
break
}
}
if (-not $found) {
Write-Host "[WARN] <$objType>$objName</$objType> 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 <v8:Value>Type.Name</v8:Value>
$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