mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-15 02:14:57 +03:00
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:
@@ -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`.
|
||||
|
||||
### Автоопределение платформы
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user