From b8e3107d147b469df33de0491697513239ef8c15 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 15 Feb 2026 16:13:55 +0300 Subject: [PATCH] feat(cfe): add configuration extension skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 5 new skills for 1C configuration extensions (CFE): - cfe-init: create extension scaffold with Configuration.xml, Languages, Roles - cfe-borrow: borrow objects from configuration with ObjectBelonging=Adopted - cfe-patch-method: generate &Перед, &После, &ИзменениеИКонтроль decorators - cfe-validate: validate extension structure with 9 checks - cfe-diff: analyze extension and check transfer status Remove ConfigDumpInfo.xml from cf-init scaffold as it's not required for 1C configuration source files. Add cfe-guide documentation. --- .claude/skills/cf-init/SKILL.md | 5 +- .claude/skills/cf-init/scripts/cf-init.ps1 | 11 - .claude/skills/cfe-borrow/SKILL.md | 15 + .claude/skills/cfe-borrow/reference.md | 48 ++ .../skills/cfe-borrow/scripts/cfe-borrow.ps1 | 587 +++++++++++++++++ .claude/skills/cfe-diff/SKILL.md | 15 + .claude/skills/cfe-diff/scripts/cfe-diff.ps1 | 392 +++++++++++ .claude/skills/cfe-init/SKILL.md | 15 + .claude/skills/cfe-init/reference.md | 57 ++ .claude/skills/cfe-init/scripts/cfe-init.ps1 | 208 ++++++ .claude/skills/cfe-patch-method/SKILL.md | 15 + .../scripts/cfe-patch-method.ps1 | 186 ++++++ .claude/skills/cfe-validate/SKILL.md | 15 + .../cfe-validate/scripts/cfe-validate.ps1 | 607 ++++++++++++++++++ README.md | 9 + docs/cfe-guide.md | 175 +++++ 16 files changed, 2346 insertions(+), 14 deletions(-) create mode 100644 .claude/skills/cfe-borrow/SKILL.md create mode 100644 .claude/skills/cfe-borrow/reference.md create mode 100644 .claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 create mode 100644 .claude/skills/cfe-diff/SKILL.md create mode 100644 .claude/skills/cfe-diff/scripts/cfe-diff.ps1 create mode 100644 .claude/skills/cfe-init/SKILL.md create mode 100644 .claude/skills/cfe-init/reference.md create mode 100644 .claude/skills/cfe-init/scripts/cfe-init.ps1 create mode 100644 .claude/skills/cfe-patch-method/SKILL.md create mode 100644 .claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 create mode 100644 .claude/skills/cfe-validate/SKILL.md create mode 100644 .claude/skills/cfe-validate/scripts/cfe-validate.ps1 create mode 100644 docs/cfe-guide.md diff --git a/.claude/skills/cf-init/SKILL.md b/.claude/skills/cf-init/SKILL.md index d603ec5a..f39cd011 100644 --- a/.claude/skills/cf-init/SKILL.md +++ b/.claude/skills/cf-init/SKILL.md @@ -1,6 +1,6 @@ --- name: cf-init -description: Создать пустую конфигурацию 1С (scaffold XML-исходников) — Configuration.xml, ConfigDumpInfo.xml, Languages/ +description: Создать пустую конфигурацию 1С (scaffold XML-исходников) — Configuration.xml, Languages/ argument-hint: [-Synonym ] [-OutputDir src] allowed-tools: - Bash @@ -10,7 +10,7 @@ allowed-tools: # /cf-init — Создание пустой конфигурации 1С -Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `ConfigDumpInfo.xml`, `Languages/Русский.xml`. +Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`. ## Параметры и команда @@ -32,7 +32,6 @@ powershell.exe -NoProfile -File .claude\skills\cf-init\scripts\cf-init.ps1 -Name ``` / ├── Configuration.xml # Корневой файл — все свойства -├── ConfigDumpInfo.xml # Служебный файл (минимальный) └── Languages/ └── Русский.xml # Язык по умолчанию ``` diff --git a/.claude/skills/cf-init/scripts/cf-init.ps1 b/.claude/skills/cf-init/scripts/cf-init.ps1 index c6b5d8b7..bce9c427 100644 --- a/.claude/skills/cf-init/scripts/cf-init.ps1 +++ b/.claude/skills/cf-init/scripts/cf-init.ps1 @@ -172,14 +172,6 @@ $cfgXml = @" "@ -# --- ConfigDumpInfo.xml --- -$dumpInfoXml = @" - - - - -"@ - # --- Languages/Русский.xml --- $langXml = @" @@ -213,8 +205,6 @@ if (-not (Test-Path $langDir)) { $enc = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) -$dumpInfoFile = Join-Path $OutputDir "ConfigDumpInfo.xml" -[System.IO.File]::WriteAllText($dumpInfoFile, $dumpInfoXml, $enc) $langFile = Join-Path $langDir "Русский.xml" [System.IO.File]::WriteAllText($langFile, $langXml, $enc) @@ -222,5 +212,4 @@ $langFile = Join-Path $langDir "Русский.xml" Write-Host "[OK] Создана конфигурация: $Name" Write-Host " Каталог: $OutputDir" Write-Host " Configuration.xml: $cfgFile" -Write-Host " ConfigDumpInfo.xml: $dumpInfoFile" Write-Host " Languages: $langFile" diff --git a/.claude/skills/cfe-borrow/SKILL.md b/.claude/skills/cfe-borrow/SKILL.md new file mode 100644 index 00000000..f0f3c629 --- /dev/null +++ b/.claude/skills/cfe-borrow/SKILL.md @@ -0,0 +1,15 @@ +--- +name: cfe-borrow +description: Заимствование объектов из конфигурации 1С в расширение (CFE) — справочники, документы, общие модули, перечисления +argument-hint: -ExtensionPath -ConfigPath -Object "Catalog.Контрагенты" +allowed-tools: + - Bash + - Read + - Glob +--- + +Заимствует объекты из конфигурации в расширение. Создаёт XML-файлы с ObjectBelonging=Adopted. + +```powershell +powershell.exe -NoProfile -File .claude\skills\cfe-borrow\scripts\cfe-borrow.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" +``` diff --git a/.claude/skills/cfe-borrow/reference.md b/.claude/skills/cfe-borrow/reference.md new file mode 100644 index 00000000..a058cc20 --- /dev/null +++ b/.claude/skills/cfe-borrow/reference.md @@ -0,0 +1,48 @@ +# /cfe-borrow — Заимствование объектов из конфигурации в расширение + +Заимствует объекты из основной конфигурации в расширение. Создаёт минимальные XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения. + +## Параметры + +| Параметр | Описание | +|----------|----------| +| `ExtensionPath` | Путь к каталогу расширения (обязат.) | +| `ConfigPath` | Путь к конфигурации-источнику (обязат.) | +| `Object` | Что заимствовать, batch через `;;` (обязат.) | + +## Формат -Object + +- `Catalog.Контрагенты` — справочник +- `CommonModule.РаботаСФайлами` — общий модуль +- `Enum.ВидыОплат` — перечисление +- `Document.РеализацияТоваров` — документ +- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — batch + +## Алгоритм + +1. Загружает Configuration.xml расширения, определяет NamePrefix +2. Загружает XML объекта из конфигурации-источника +3. Создаёт XML заимствованного объекта: + - `ObjectBelonging: Adopted` + - `ExtendedConfigurationObject: ` + - `Name: <имя из конфигурации>` + - `InternalInfo` с `GeneratedType` (копируется из конфигурации) +4. Добавляет в `ChildObjects` расширения в каноническую позицию +5. Создаёт каталог и XML-файл + +## Примеры + +```powershell +# Заимствовать справочник +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" + +# Несколько объектов +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодульСервер ;; Enum.ВидыОплат" +``` + +## Верификация + +``` +/cfe-borrow -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" +/cfe-validate src — проверить результат +``` diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 new file mode 100644 index 00000000..fcb0a76f --- /dev/null +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -0,0 +1,587 @@ +# cfe-borrow v1.0 — Borrow objects from configuration into extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)][string]$ExtensionPath, + [Parameter(Mandatory)][string]$ConfigPath, + [Parameter(Mandatory)][string]$Object +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +function Info([string]$msg) { Write-Host "[INFO] $msg" } +function Warn([string]$msg) { Write-Host "[WARN] $msg" } + +# --- 1. Resolve paths --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} +if (Test-Path $ExtensionPath -PathType Container) { + $candidate = Join-Path $ExtensionPath "Configuration.xml" + if (Test-Path $candidate) { $ExtensionPath = $candidate } + else { Write-Error "No Configuration.xml in extension directory: $ExtensionPath"; exit 1 } +} +if (-not (Test-Path $ExtensionPath)) { Write-Error "Extension file not found: $ExtensionPath"; exit 1 } +$extResolvedPath = (Resolve-Path $ExtensionPath).Path +$extDir = Split-Path $extResolvedPath -Parent + +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} +if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { $ConfigPath = $candidate } + else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 } +} +if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 } +$cfgResolvedPath = (Resolve-Path $ConfigPath).Path +$cfgDir = Split-Path $cfgResolvedPath -Parent + +# --- 2. Load extension Configuration.xml --- +$script:xmlDoc = New-Object System.Xml.XmlDocument +$script:xmlDoc.PreserveWhitespace = $true +$script:xmlDoc.Load($extResolvedPath) + +$script:mdNs = "http://v8.1c.ru/8.3/MDClasses" +$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" +$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance" +$script:v8Ns = "http://v8.1c.ru/8.1/data/core" + +$root = $script:xmlDoc.DocumentElement + +$script:cfgEl = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration") { + $script:cfgEl = $child; break + } +} +if (-not $script:cfgEl) { Write-Error "No element found in extension"; exit 1 } + +$script:propsEl = $null +$script:childObjsEl = $null +foreach ($child in $script:cfgEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq "Properties") { $script:propsEl = $child } + if ($child.LocalName -eq "ChildObjects") { $script:childObjsEl = $child } +} + +if (-not $script:propsEl) { Write-Error "No element found in extension"; exit 1 } +if (-not $script:childObjsEl) { Write-Error "No element found in extension"; exit 1 } + +# --- 3. Extract NamePrefix --- +$script:namePrefix = "" +foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "NamePrefix") { + $script:namePrefix = $child.InnerText.Trim(); break + } +} +Info "Extension NamePrefix: $($script:namePrefix)" + +# --- 4. Type mappings --- +$childTypeDirMap = @{ + "Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums" + "CommonModule"="CommonModules"; "CommonPicture"="CommonPictures" + "CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates" + "ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors" + "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" + "Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants" + "FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes" + "FunctionalOptionsParameter"="FunctionalOptionsParameters" + "CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals" + "SessionParameter"="SessionParameters"; "StyleItem"="StyleItems" + "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" + "SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria" + "CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators" + "Sequence"="Sequences"; "IntegrationService"="IntegrationServices" + "XDTOPackage"="XDTOPackages"; "WebService"="WebServices" + "HTTPService"="HTTPServices"; "WSReference"="WSReferences" + "CommonAttribute"="CommonAttributes"; "Style"="Styles" +} + +# --- 5. Canonical type order (44 types) --- +$script:typeOrder = @( + "Language","Subsystem","StyleItem","Style", + "CommonPicture","SessionParameter","Role","CommonTemplate", + "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", + "XDTOPackage","WebService","HTTPService","WSReference", + "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", + "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", + "Constant","CommonForm","Catalog","Document", + "DocumentNumerator","Sequence","DocumentJournal","Enum", + "Report","DataProcessor","InformationRegister","AccumulationRegister", + "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", + "ChartOfCalculationTypes","CalculationRegister", + "BusinessProcess","Task","IntegrationService" +) + +# --- 6. GeneratedType patterns per type --- +$script:generatedTypes = @{ + "Catalog" = @( + @{ prefix = "CatalogObject"; category = "Object" } + @{ prefix = "CatalogRef"; category = "Ref" } + @{ prefix = "CatalogSelection"; category = "Selection" } + @{ prefix = "CatalogList"; category = "List" } + @{ prefix = "CatalogManager"; category = "Manager" } + ) + "Document" = @( + @{ prefix = "DocumentObject"; category = "Object" } + @{ prefix = "DocumentRef"; category = "Ref" } + @{ prefix = "DocumentSelection"; category = "Selection" } + @{ prefix = "DocumentList"; category = "List" } + @{ prefix = "DocumentManager"; category = "Manager" } + ) + "Enum" = @( + @{ prefix = "EnumRef"; category = "Ref" } + @{ prefix = "EnumManager"; category = "Manager" } + @{ prefix = "EnumList"; category = "List" } + ) + "Constant" = @( + @{ prefix = "ConstantManager"; category = "Manager" } + @{ prefix = "ConstantValueManager"; category = "ValueManager" } + @{ prefix = "ConstantValueKey"; category = "ValueKey" } + ) + "InformationRegister" = @( + @{ prefix = "InformationRegisterRecord"; category = "Record" } + @{ prefix = "InformationRegisterManager"; category = "Manager" } + @{ prefix = "InformationRegisterSelection"; category = "Selection" } + @{ prefix = "InformationRegisterList"; category = "List" } + @{ prefix = "InformationRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "InformationRegisterRecordKey"; category = "RecordKey" } + @{ prefix = "InformationRegisterRecordManager"; category = "RecordManager" } + ) + "AccumulationRegister" = @( + @{ prefix = "AccumulationRegisterRecord"; category = "Record" } + @{ prefix = "AccumulationRegisterManager"; category = "Manager" } + @{ prefix = "AccumulationRegisterSelection"; category = "Selection" } + @{ prefix = "AccumulationRegisterList"; category = "List" } + @{ prefix = "AccumulationRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" } + ) + "AccountingRegister" = @( + @{ prefix = "AccountingRegisterRecord"; category = "Record" } + @{ prefix = "AccountingRegisterManager"; category = "Manager" } + @{ prefix = "AccountingRegisterSelection"; category = "Selection" } + @{ prefix = "AccountingRegisterList"; category = "List" } + @{ prefix = "AccountingRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "AccountingRegisterRecordKey"; category = "RecordKey" } + ) + "CalculationRegister" = @( + @{ prefix = "CalculationRegisterRecord"; category = "Record" } + @{ prefix = "CalculationRegisterManager"; category = "Manager" } + @{ prefix = "CalculationRegisterSelection"; category = "Selection" } + @{ prefix = "CalculationRegisterList"; category = "List" } + @{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" } + ) + "ChartOfAccounts" = @( + @{ prefix = "ChartOfAccountsObject"; category = "Object" } + @{ prefix = "ChartOfAccountsRef"; category = "Ref" } + @{ prefix = "ChartOfAccountsSelection"; category = "Selection" } + @{ prefix = "ChartOfAccountsList"; category = "List" } + @{ prefix = "ChartOfAccountsManager"; category = "Manager" } + ) + "ChartOfCharacteristicTypes" = @( + @{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" } + @{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" } + @{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" } + @{ prefix = "ChartOfCharacteristicTypesList"; category = "List" } + @{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" } + ) + "ChartOfCalculationTypes" = @( + @{ prefix = "ChartOfCalculationTypesObject"; category = "Object" } + @{ prefix = "ChartOfCalculationTypesRef"; category = "Ref" } + @{ prefix = "ChartOfCalculationTypesSelection"; category = "Selection" } + @{ prefix = "ChartOfCalculationTypesList"; category = "List" } + @{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" } + @{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" } + @{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" } + @{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" } + ) + "BusinessProcess" = @( + @{ prefix = "BusinessProcessObject"; category = "Object" } + @{ prefix = "BusinessProcessRef"; category = "Ref" } + @{ prefix = "BusinessProcessSelection"; category = "Selection" } + @{ prefix = "BusinessProcessList"; category = "List" } + @{ prefix = "BusinessProcessManager"; category = "Manager" } + ) + "Task" = @( + @{ prefix = "TaskObject"; category = "Object" } + @{ prefix = "TaskRef"; category = "Ref" } + @{ prefix = "TaskSelection"; category = "Selection" } + @{ prefix = "TaskList"; category = "List" } + @{ prefix = "TaskManager"; category = "Manager" } + ) + "ExchangePlan" = @( + @{ prefix = "ExchangePlanObject"; category = "Object" } + @{ prefix = "ExchangePlanRef"; category = "Ref" } + @{ prefix = "ExchangePlanSelection"; category = "Selection" } + @{ prefix = "ExchangePlanList"; category = "List" } + @{ prefix = "ExchangePlanManager"; category = "Manager" } + ) + "DocumentJournal" = @( + @{ prefix = "DocumentJournalSelection"; category = "Selection" } + @{ prefix = "DocumentJournalList"; category = "List" } + @{ prefix = "DocumentJournalManager"; category = "Manager" } + ) + "Report" = @( + @{ prefix = "ReportObject"; category = "Object" } + ) + "DataProcessor" = @( + @{ prefix = "DataProcessorObject"; category = "Object" } + ) +} + +# Types that need ChildObjects element +$typesWithChildObjects = @( + "Catalog","Document","ExchangePlan","ChartOfAccounts", + "ChartOfCharacteristicTypes","ChartOfCalculationTypes", + "BusinessProcess","Task","Enum", + "InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister" +) + +# CommonModule properties to copy from source +$commonModuleProps = @("Global","ClientManagedApplication","Server","ExternalConnection","ClientOrdinaryApplication","ServerCall") + +# --- 7. XML manipulation helpers (from cf-edit) --- +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + $depth = 0; $current = $container + while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode } + return "`t" * ($depth + 1) +} + +function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { + $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $ws) | Out-Null + } else { + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $trailing) | Out-Null + } else { + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } + } +} + +function Expand-SelfClosingElement($container, $parentIndent) { + if (-not $container.HasChildNodes -or $container.IsEmpty) { + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } +} + +# --- 8. Namespaces declaration for object XML --- +$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' + +# --- 9. Parse -Object into items --- +$items = @() +foreach ($part in $Object.Split(";;")) { + $trimmed = $part.Trim() + if ($trimmed) { $items += $trimmed } +} + +if ($items.Count -eq 0) { + Write-Error "No objects specified in -Object" + exit 1 +} + +# --- 10. Helper: read source object XML --- +function Read-SourceObject { + param([string]$typeName, [string]$objName) + + $dirName = $childTypeDirMap[$typeName] + if (-not $dirName) { + Write-Error "Unknown type '$typeName'" + exit 1 + } + + $srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml" + if (-not (Test-Path $srcFile)) { + Write-Error "Source object not found: $srcFile" + exit 1 + } + + $srcDoc = New-Object System.Xml.XmlDocument + $srcDoc.PreserveWhitespace = $false + $srcDoc.Load($srcFile) + + $srcNs = New-Object System.Xml.XmlNamespaceManager($srcDoc.NameTable) + $srcNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $srcNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + + # Find the type element (e.g. ) + $srcRoot = $srcDoc.DocumentElement + $srcEl = $null + foreach ($c in $srcRoot.ChildNodes) { + if ($c.NodeType -eq 'Element') { $srcEl = $c; break } + } + if (-not $srcEl) { + Write-Error "No metadata element found in ${dirName}/${objName}.xml" + exit 1 + } + + # Extract uuid + $srcUuid = $srcEl.GetAttribute("uuid") + if (-not $srcUuid) { + Write-Error "No uuid attribute on source element in ${dirName}/${objName}.xml" + exit 1 + } + + # Extract properties for CommonModule + $srcProps = @{} + $propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs) + if ($propsNode) { + foreach ($propName in $commonModuleProps) { + $propNode = $propsNode.SelectSingleNode("md:${propName}", $srcNs) + if ($propNode) { + $srcProps[$propName] = $propNode.InnerText.Trim() + } + } + } + + return @{ + Uuid = $srcUuid + Properties = $srcProps + Element = $srcEl + NsManager = $srcNs + } +} + +# --- 11. Helper: generate InternalInfo XML --- +function Build-InternalInfoXml { + param([string]$typeName, [string]$objName, [string]$indent) + + $types = $script:generatedTypes[$typeName] + if (-not $types -or $types.Count -eq 0) { + return "${indent}" + } + + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("${indent}") | Out-Null + + # ExchangePlan: ThisNode UUID before GeneratedTypes + if ($typeName -eq "ExchangePlan") { + $thisNodeUuid = [guid]::NewGuid().ToString() + $sb.AppendLine("${indent}`t${thisNodeUuid}") | Out-Null + } + + foreach ($gt in $types) { + $fullName = "$($gt.prefix).${objName}" + $typeId = [guid]::NewGuid().ToString() + $valueId = [guid]::NewGuid().ToString() + $sb.AppendLine("${indent}`t") | Out-Null + $sb.AppendLine("${indent}`t`t${typeId}") | Out-Null + $sb.AppendLine("${indent}`t`t${valueId}") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + } + + $sb.Append("${indent}") | Out-Null + return $sb.ToString() +} + +# --- 12. Helper: build borrowed object XML --- +function Build-BorrowedObjectXml { + param( + [string]$typeName, + [string]$objName, + [string]$sourceUuid, + [hashtable]$sourceProps + ) + + $newUuid = [guid]::NewGuid().ToString() + $internalInfoXml = Build-InternalInfoXml $typeName $objName "`t`t" + + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("") | Out-Null + $sb.AppendLine("") | Out-Null + $sb.AppendLine("`t<${typeName} uuid=`"${newUuid}`">") | Out-Null + + # InternalInfo + $sb.AppendLine($internalInfoXml) | Out-Null + + # Properties + $sb.AppendLine("`t`t") | Out-Null + $sb.AppendLine("`t`t`tAdopted") | Out-Null + $sb.AppendLine("`t`t`t${objName}") | Out-Null + $sb.AppendLine("`t`t`t") | Out-Null + $sb.AppendLine("`t`t`t${sourceUuid}") | Out-Null + + # CommonModule: extra properties from source + if ($typeName -eq "CommonModule") { + foreach ($propName in $commonModuleProps) { + $propVal = "false" + if ($sourceProps.ContainsKey($propName)) { + $propVal = $sourceProps[$propName] + } + $sb.AppendLine("`t`t`t<${propName}>${propVal}") | Out-Null + } + } + + $sb.AppendLine("`t`t") | Out-Null + + # ChildObjects (for types that need it) + if ($typesWithChildObjects -contains $typeName) { + $sb.AppendLine("`t`t") | Out-Null + } + + $sb.AppendLine("`t") | Out-Null + $sb.Append("") | Out-Null + + return $sb.ToString() +} + +# --- 13. Helper: add object to extension ChildObjects --- +function Add-ToChildObjects { + param([string]$typeName, [string]$objName) + + $cfgIndent = Get-ChildIndent $script:cfgEl + + # Expand self-closing ChildObjects if needed + if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) { + Expand-SelfClosingElement $script:childObjsEl $cfgIndent + } + $childIndent = Get-ChildIndent $script:childObjsEl + + $typeIdx = $script:typeOrder.IndexOf($typeName) + if ($typeIdx -lt 0) { + Write-Error "Unknown type '$typeName' for ChildObjects ordering" + exit 1 + } + + # Dedup check + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objName) { + Warn "Already in ChildObjects: ${typeName}.${objName}" + return + } + } + + # Find insertion point: after last element of same type, or before first element of later type + $insertBefore = $null + $lastSameType = $null + + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childTypeIdx = $script:typeOrder.IndexOf($child.LocalName) + if ($childTypeIdx -lt 0) { continue } + + if ($child.LocalName -eq $typeName) { + # Same type -- check alphabetical order + if ($child.InnerText -gt $objName -and -not $insertBefore) { + $insertBefore = $child + } + $lastSameType = $child + } elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) { + # First element of a later type -- insert before it + $insertBefore = $child + } + } + + # Create element + $newEl = $script:xmlDoc.CreateElement($typeName, $script:mdNs) + $newEl.InnerText = $objName + + if ($insertBefore) { + Insert-BeforeElement $script:childObjsEl $newEl $insertBefore $childIndent + } else { + Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent + } + + Info "Added to ChildObjects: ${typeName}.${objName}" +} + +# --- 14. Process each item --- +$borrowedFiles = @() +$borrowedCount = 0 + +foreach ($item in $items) { + $dotIdx = $item.IndexOf(".") + if ($dotIdx -lt 1) { + Write-Error "Invalid format '${item}', expected 'Type.Name'" + exit 1 + } + $typeName = $item.Substring(0, $dotIdx) + $objName = $item.Substring($dotIdx + 1) + + if (-not $childTypeDirMap.ContainsKey($typeName)) { + Write-Error "Unknown type '${typeName}'" + exit 1 + } + + $dirName = $childTypeDirMap[$typeName] + + Info "Borrowing ${typeName}.${objName}..." + + # Read source object + $src = Read-SourceObject $typeName $objName + Info " Source UUID: $($src.Uuid)" + + # Build borrowed object XML + $borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties + + # Create directory in extension if needed + $targetDir = Join-Path $extDir $dirName + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + + # Write borrowed object XML with UTF-8 BOM + $targetFile = Join-Path $targetDir "${objName}.xml" + $enc = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc) + Info " Created: $targetFile" + + # Add to ChildObjects + Add-ToChildObjects $typeName $objName + + $borrowedFiles += $targetFile + $borrowedCount++ +} + +# --- 15. Save modified Configuration.xml --- +$settings = New-Object System.Xml.XmlWriterSettings +$settings.Encoding = New-Object System.Text.UTF8Encoding($true) +$settings.Indent = $false +$settings.NewLineHandling = [System.Xml.NewLineHandling]::None + +$memStream = New-Object System.IO.MemoryStream +$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) +$script:xmlDoc.Save($writer) +$writer.Flush(); $writer.Close() + +$bytes = $memStream.ToArray() +$memStream.Close() +$text = [System.Text.Encoding]::UTF8.GetString($bytes) +if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } +$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') + +$utf8Bom = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom) +Info "Saved: $extResolvedPath" + +# --- 16. Summary --- +Write-Host "" +Write-Host "=== cfe-borrow summary ===" +Write-Host " Extension: $extDir" +Write-Host " Config: $cfgDir" +Write-Host " Borrowed: $borrowedCount object(s)" +foreach ($f in $borrowedFiles) { + Write-Host " - $f" +} +exit 0 diff --git a/.claude/skills/cfe-diff/SKILL.md b/.claude/skills/cfe-diff/SKILL.md new file mode 100644 index 00000000..01a0be95 --- /dev/null +++ b/.claude/skills/cfe-diff/SKILL.md @@ -0,0 +1,15 @@ +--- +name: cfe-diff +description: Анализ и сравнение расширения конфигурации 1С (CFE) — обзор изменений, проверка переноса +argument-hint: -ExtensionPath -ConfigPath [-Mode A|B] +allowed-tools: + - Bash + - Read + - Glob +--- + +Анализирует расширение: Mode A — обзор изменений, Mode B — проверка переноса в конфигурацию. + +```powershell +powershell.exe -NoProfile -File .claude\skills\cfe-diff\scripts\cfe-diff.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A +``` diff --git a/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 b/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 new file mode 100644 index 00000000..4177d184 --- /dev/null +++ b/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 @@ -0,0 +1,392 @@ +# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ExtensionPath, + + [Parameter(Mandatory)] + [string]$ConfigPath, + + [ValidateSet("A","B")] + [string]$Mode = "A" +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve paths --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} +if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent } +if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent } + +$extCfg = Join-Path $ExtensionPath "Configuration.xml" +$srcCfg = Join-Path $ConfigPath "Configuration.xml" +if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 } +if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 } + +# --- Type -> directory mapping --- +$childTypeDirMap = @{ + "Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums" + "CommonModule"="CommonModules"; "CommonPicture"="CommonPictures" + "CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates" + "ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors" + "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" + "Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants" + "FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes" + "FunctionalOptionsParameter"="FunctionalOptionsParameters" + "CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals" + "SessionParameter"="SessionParameters"; "StyleItem"="StyleItems" + "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" + "SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria" + "CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators" + "Sequence"="Sequences"; "IntegrationService"="IntegrationServices" + "CommonAttribute"="CommonAttributes" +} + +# --- Parse extension Configuration.xml --- +$extDoc = New-Object System.Xml.XmlDocument +$extDoc.PreserveWhitespace = $false +$extDoc.Load($extCfg) + +$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + +$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns) +$extNameNode = $extProps.SelectSingleNode("md:Name", $ns) +$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" } +$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns) +$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" } +$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns) +$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" } + +Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ===" +Write-Host " NamePrefix: $namePrefix" +Write-Host "" + +# --- Collect ChildObjects --- +$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns) +if (-not $childObjNode) { + Write-Host "[WARN] No ChildObjects in extension" + exit 0 +} + +$objects = @() +foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq "Language") { continue } + $objects += @{ Type = $child.LocalName; Name = $child.InnerText } +} + +if ($objects.Count -eq 0) { + Write-Host "No objects (besides Language) in extension." + exit 0 +} + +# --- Helper: check if object is borrowed --- +function Get-ObjectInfo { + param([string]$objType, [string]$objName) + + if (-not $childTypeDirMap.ContainsKey($objType)) { return $null } + $dirName = $childTypeDirMap[$objType] + $objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml" + + if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } } + + $doc = New-Object System.Xml.XmlDocument + $doc.PreserveWhitespace = $false + $doc.Load($objFile) + + $objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) + $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $objEl = $null + foreach ($c in $doc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } } + + $propsEl = $objEl.SelectSingleNode("md:Properties", $objNs) + $obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null } + + $info = @{ + Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted") + File = $objFile + Exists = $true + Type = $objType + Name = $objName + DirName = $dirName + ObjElement = $objEl + ObjNs = $objNs + } + return $info +} + +# --- Helper: find .bsl files for object --- +function Get-BslFiles { + param([string]$objType, [string]$objName) + + if (-not $childTypeDirMap.ContainsKey($objType)) { return @() } + $dirName = $childTypeDirMap[$objType] + $objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName + + if (-not (Test-Path $objDir -PathType Container)) { return @() } + + $bslFiles = @() + $extDir = Join-Path $objDir "Ext" + if (Test-Path $extDir) { + $items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue + foreach ($item in $items) { $bslFiles += $item.FullName } + } + + # Forms + $formsDir = Join-Path $objDir "Forms" + if (Test-Path $formsDir) { + $formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue + foreach ($fm in $formModules) { $bslFiles += $fm.FullName } + } + + return $bslFiles +} + +# --- Helper: parse interceptors from .bsl --- +function Get-Interceptors { + param([string]$bslPath) + + if (-not (Test-Path $bslPath)) { return @() } + $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) + $interceptors = @() + $i = 0 + while ($i -lt $lines.Count) { + $line = $lines[$i].Trim() + if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') { + $type = $Matches[1] + $method = $Matches[2] + $interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath } + } + $i++ + } + return $interceptors +} + +# --- Helper: extract #Вставка blocks from .bsl --- +function Get-InsertionBlocks { + param([string]$bslPath) + + if (-not (Test-Path $bslPath)) { return @() } + $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) + $blocks = @() + $inBlock = $false + $blockLines = @() + $startLine = 0 + + for ($i = 0; $i -lt $lines.Count; $i++) { + $line = $lines[$i].Trim() + if ($line -eq "#Вставка") { + $inBlock = $true + $blockLines = @() + $startLine = $i + 1 + } elseif ($line -eq "#КонецВставки" -and $inBlock) { + $inBlock = $false + $blocks += @{ + StartLine = $startLine + EndLine = $i + 1 + Code = ($blockLines -join "`n").Trim() + File = $bslPath + } + } elseif ($inBlock) { + $blockLines += $lines[$i] + } + } + return $blocks +} + +# ============================================================ +# MODE A: Extension overview +# ============================================================ +if ($Mode -eq "A") { + $borrowedList = @() + $ownList = @() + + foreach ($obj in $objects) { + $info = Get-ObjectInfo $obj.Type $obj.Name + if (-not $info) { + Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type" + continue + } + if (-not $info.Exists) { + Write-Host " [?] $($obj.Type).$($obj.Name) — file not found" + continue + } + + if ($info.Borrowed) { + $borrowedList += $obj + + Write-Host " [BORROWED] $($obj.Type).$($obj.Name)" + + # Find .bsl files and interceptors + $bslFiles = Get-BslFiles $obj.Type $obj.Name + foreach ($bsl in $bslFiles) { + $relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") + $interceptors = Get-Interceptors $bsl + if ($interceptors.Count -gt 0) { + foreach ($ic in $interceptors) { + Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath" + } + } else { + Write-Host " $relPath (no interceptors)" + } + } + + # Check for own attributes/forms in ChildObjects + if ($info.ObjElement) { + $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) + if ($childObj) { + $ownAttrs = 0 + $ownForms = 0 + $ownTS = 0 + $borrowedItems = 0 + foreach ($c in $childObj.ChildNodes) { + if ($c.NodeType -ne 'Element') { continue } + $cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs) + if ($cProps) { + $cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs) + if ($cOb -and $cOb.InnerText -eq "Adopted") { + $borrowedItems++ + continue + } + } + switch ($c.LocalName) { + "Attribute" { $ownAttrs++ } + "TabularSection" { $ownTS++ } + "Form" { $ownForms++ } + } + } + $parts = @() + if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" } + if ($ownTS -gt 0) { $parts += "$ownTS own TS" } + if ($ownForms -gt 0) { $parts += "$ownForms own forms" } + if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" } + if ($parts.Count -gt 0) { + Write-Host " ChildObjects: $($parts -join ', ')" + } + } + } + } else { + $ownList += $obj + Write-Host " [OWN] $($obj.Type).$($obj.Name)" + + # Brief info for own objects + if ($info.ObjElement) { + $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) + if ($childObj) { + $attrs = 0; $forms = 0; $ts = 0 + foreach ($c in $childObj.ChildNodes) { + if ($c.NodeType -ne 'Element') { continue } + switch ($c.LocalName) { + "Attribute" { $attrs++ } + "TabularSection" { $ts++ } + "Form" { $forms++ } + } + } + $parts = @() + if ($attrs -gt 0) { $parts += "$attrs attrs" } + if ($ts -gt 0) { $parts += "$ts TS" } + if ($forms -gt 0) { $parts += "$forms forms" } + if ($parts.Count -gt 0) { + Write-Host " $($parts -join ', ')" + } + } + } + } + } + + Write-Host "" + Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ===" +} + +# ============================================================ +# MODE B: Transfer check +# ============================================================ +if ($Mode -eq "B") { + $transferred = 0 + $notTransferred = 0 + $needsReview = 0 + + foreach ($obj in $objects) { + $info = Get-ObjectInfo $obj.Type $obj.Name + if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue } + + # Find .bsl files with &ИзменениеИКонтроль + $bslFiles = Get-BslFiles $obj.Type $obj.Name + foreach ($bsl in $bslFiles) { + $interceptors = Get-Interceptors $bsl + $macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" }) + + if ($macInterceptors.Count -eq 0) { continue } + + foreach ($ic in $macInterceptors) { + $methodName = $ic.Method + $relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") + + # Find #Вставка blocks in this file + $insertBlocks = Get-InsertionBlocks $bsl + + if ($insertBlocks.Count -eq 0) { + Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks" + $needsReview++ + continue + } + + # Find corresponding module in config + if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue } + $dirName = $childTypeDirMap[$obj.Type] + $configBsl = $bsl.Replace($ExtensionPath, $ConfigPath) + + if (-not (Test-Path $configBsl)) { + Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found" + $needsReview++ + continue + } + + $configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8) + + $allTransferred = $true + foreach ($block in $insertBlocks) { + $code = $block.Code + if (-not $code) { continue } + + # Normalize whitespace for comparison + $codeNorm = $code -replace '\s+', ' ' + $configNorm = $configContent -replace '\s+', ' ' + + if ($configNorm.Contains($codeNorm)) { + # Found in config + } else { + $allTransferred = $false + } + } + + if ($allTransferred) { + Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)" + $transferred++ + } else { + Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config" + $notTransferred++ + } + } + } + } + + Write-Host "" + Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ===" +} diff --git a/.claude/skills/cfe-init/SKILL.md b/.claude/skills/cfe-init/SKILL.md new file mode 100644 index 00000000..6449046f --- /dev/null +++ b/.claude/skills/cfe-init/SKILL.md @@ -0,0 +1,15 @@ +--- +name: cfe-init +description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников расширения +argument-hint: [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24] +allowed-tools: + - Bash + - Read + - Glob +--- + +Создаёт scaffold расширения конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`. + +```powershell +powershell.exe -NoProfile -File .claude\skills\cfe-init\scripts\cfe-init.ps1 -Name "МоёРасширение" +``` diff --git a/.claude/skills/cfe-init/reference.md b/.claude/skills/cfe-init/reference.md new file mode 100644 index 00000000..94aee7ba --- /dev/null +++ b/.claude/skills/cfe-init/reference.md @@ -0,0 +1,57 @@ +# /cfe-init — Создание расширения конфигурации 1С (CFE) + +Создаёт scaffold исходников расширения конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`. + +## Рекомендуемый пайплайн + +Перед вызовом cfe-init рекомендуется запустить `cf-info -ConfigPath <путь к конфигурации> -Mode brief`, чтобы получить: +- `CompatibilityMode` → передать в `-CompatibilityMode` +- Версию конфигурации → по умолчанию `-Version` = `<ВерсияКонфигурации>.1` + +## Параметры + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `Name` | Имя расширения (обязат.) | — | +| `Synonym` | Синоним | = Name | +| `NamePrefix` | Префикс собственных объектов | = Name + "_" | +| `OutputDir` | Каталог для создания | `src` | +| `Purpose` | Назначение: `Patch` / `Customization` / `AddOn` | `Customization` | +| `Version` | Версия расширения | — | +| `Vendor` | Поставщик | — | +| `CompatibilityMode` | Режим совместимости | `Version8_3_24` | +| `NoRole` | Без основной роли | false | + +## Что создаётся + +``` +/ +├── Configuration.xml # Свойства расширения +├── Languages/ +│ └── Русский.xml # Язык (заимствованный формат) +└── Roles/ # Если не -NoRole + └── ОсновнаяРоль.xml +``` + +## Примеры + +```powershell +# Расширение-исправление для ERP +... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir test-tmp/cfe + +# Расширение-адаптация с версией +... -Name МоёРасширение -Purpose Customization -Version "1.0.0.1" -Vendor "Компания" -OutputDir test-tmp/cfe2 + +# Без роли +... -Name Расш2 -Purpose AddOn -NoRole -OutputDir test-tmp/cfe3 + +# С явным префиксом +... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -OutputDir test-tmp/cfe4 +``` + +## Верификация + +``` +/cfe-init МоёРасширение -OutputDir test-tmp/cfe +/cfe-validate test-tmp/cfe — валидировать +``` diff --git a/.claude/skills/cfe-init/scripts/cfe-init.ps1 b/.claude/skills/cfe-init/scripts/cfe-init.ps1 new file mode 100644 index 00000000..b8f449ad --- /dev/null +++ b/.claude/skills/cfe-init/scripts/cfe-init.ps1 @@ -0,0 +1,208 @@ +# cfe-init v1.0 — Create 1C configuration extension scaffold (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$Name, + [string]$Synonym = $Name, + [string]$NamePrefix, + [string]$OutputDir = "src", + [ValidateSet("Patch","Customization","AddOn")] + [string]$Purpose = "Customization", + [string]$Version, + [string]$Vendor, + [string]$CompatibilityMode = "Version8_3_24", + [switch]$NoRole +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Default NamePrefix --- +if (-not $NamePrefix) { + $NamePrefix = "${Name}_" +} + +# --- Resolve output dir --- +if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { + $OutputDir = Join-Path (Get-Location).Path $OutputDir +} + +# --- Check existing --- +$cfgFile = Join-Path $OutputDir "Configuration.xml" +if (Test-Path $cfgFile) { + Write-Error "Configuration.xml already exists: $cfgFile" + exit 1 +} + +# --- Generate UUIDs --- +$uuidCfg = [guid]::NewGuid().ToString() +$uuidLang = [guid]::NewGuid().ToString() +$uuidRole = [guid]::NewGuid().ToString() + +# 7 ContainedObject ObjectIds +$co1 = [guid]::NewGuid().ToString() +$co2 = [guid]::NewGuid().ToString() +$co3 = [guid]::NewGuid().ToString() +$co4 = [guid]::NewGuid().ToString() +$co5 = [guid]::NewGuid().ToString() +$co6 = [guid]::NewGuid().ToString() +$co7 = [guid]::NewGuid().ToString() + +# --- Synonym XML --- +$synonymXml = "" +if ($Synonym) { + $synonymXml = "`r`n`t`t`t`t`r`n`t`t`t`t`tru`r`n`t`t`t`t`t$([System.Security.SecurityElement]::Escape($Synonym))`r`n`t`t`t`t`r`n`t`t`t" +} + +# --- Optional properties --- +$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } +$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" } + +# --- Role name --- +$roleName = "${NamePrefix}ОсновнаяРоль" + +# --- DefaultRoles XML --- +$defaultRolesXml = "" +if (-not $NoRole) { + $defaultRolesXml = "`r`n`t`t`t`tRole.$roleName`r`n`t`t`t" +} + +# --- ChildObjects --- +$childObjectsXml = "`r`n`t`t`tРусский" +if (-not $NoRole) { + $childObjectsXml += "`r`n`t`t`t$roleName" +} +$childObjectsXml += "`r`n`t`t" + +# --- Configuration.xml --- +$cfgXml = @" + + + + + + 9cd510cd-abfc-11d4-9434-004095e12fc7 + $co1 + + + 9fcd25a0-4822-11d4-9414-008048da11f9 + $co2 + + + e3687481-0a87-462c-a166-9f34594f9bba + $co3 + + + 9de14907-ec23-4a07-96f0-85521cb6b53b + $co4 + + + 51f2d5d8-ea4d-4064-8892-82951750031e + $co5 + + + e68182ea-4237-4383-967f-90c1e3370bc7 + $co6 + + + fb282519-d103-4dd3-bc12-cb271d631dfc + $co7 + + + + Adopted + $([System.Security.SecurityElement]::Escape($Name)) + $synonymXml + + $Purpose + true + $([System.Security.SecurityElement]::Escape($NamePrefix)) + $CompatibilityMode + ManagedApplication + + PlatformApplication + + Russian + $defaultRolesXml + $vendorXml + $versionXml + Language.Русский + + + + + + TaxiEnableVersion8_2 + + $childObjectsXml + + +"@ + +# --- Languages/Русский.xml (adopted format) --- +$langXml = @" + + + + + + Adopted + Русский + + 00000000-0000-0000-0000-000000000000 + ru + + + +"@ + +# --- Role XML --- +$roleXml = @" + + + + + $([System.Security.SecurityElement]::Escape($roleName)) + + + + + +"@ + +# --- Create directories --- +if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} +$langDir = Join-Path $OutputDir "Languages" +if (-not (Test-Path $langDir)) { + New-Item -ItemType Directory -Path $langDir -Force | Out-Null +} + +# --- Write files with UTF-8 BOM --- +$enc = New-Object System.Text.UTF8Encoding($true) + +[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) +$langFile = Join-Path $langDir "Русский.xml" +[System.IO.File]::WriteAllText($langFile, $langXml, $enc) + +# --- Role --- +if (-not $NoRole) { + $roleDir = Join-Path $OutputDir "Roles" + if (-not (Test-Path $roleDir)) { + New-Item -ItemType Directory -Path $roleDir -Force | Out-Null + } + $roleFile = Join-Path $roleDir "$roleName.xml" + [System.IO.File]::WriteAllText($roleFile, $roleXml, $enc) +} + +# --- Output --- +Write-Host "[OK] Создано расширение: $Name" +Write-Host " Каталог: $OutputDir" +Write-Host " Назначение: $Purpose" +Write-Host " Префикс: $NamePrefix" +Write-Host " Configuration.xml: $cfgFile" +Write-Host " Languages: $langFile" +if (-not $NoRole) { + Write-Host " Role: $roleFile" +} diff --git a/.claude/skills/cfe-patch-method/SKILL.md b/.claude/skills/cfe-patch-method/SKILL.md new file mode 100644 index 00000000..ef5198d7 --- /dev/null +++ b/.claude/skills/cfe-patch-method/SKILL.md @@ -0,0 +1,15 @@ +--- +name: cfe-patch-method +description: Генерация перехватчика метода в расширении 1С (CFE) — &Перед, &После, &ИзменениеИКонтроль +argument-hint: -ExtensionPath -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before +allowed-tools: + - Bash + - Read + - Glob +--- + +Генерирует .bsl файл с декоратором перехвата для заимствованного объекта расширения. + +```powershell +powershell.exe -NoProfile -File .claude\skills\cfe-patch-method\scripts\cfe-patch-method.ps1 -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before +``` diff --git a/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 new file mode 100644 index 00000000..7c3bf2d0 --- /dev/null +++ b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 @@ -0,0 +1,186 @@ +# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ExtensionPath, + + [Parameter(Mandatory)] + [string]$ModulePath, + + [Parameter(Mandatory)] + [string]$MethodName, + + [Parameter(Mandatory)] + [ValidateSet("Before","After","ModificationAndControl")] + [string]$InterceptorType, + + [string]$Context = "НаСервере", + + [switch]$IsFunction +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve extension path --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} +if (Test-Path $ExtensionPath -PathType Leaf) { + $ExtensionPath = Split-Path $ExtensionPath -Parent +} +$cfgFile = Join-Path $ExtensionPath "Configuration.xml" +if (-not (Test-Path $cfgFile)) { + Write-Error "Configuration.xml not found in: $ExtensionPath" + exit 1 +} + +# --- Read NamePrefix from Configuration.xml --- +$cfgDoc = New-Object System.Xml.XmlDocument +$cfgDoc.PreserveWhitespace = $false +$cfgDoc.Load($cfgFile) + +$cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgDoc.NameTable) +$cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + +$propsNode = $cfgDoc.SelectSingleNode("//md:Configuration/md:Properties", $cfgNs) +$prefixNode = if ($propsNode) { $propsNode.SelectSingleNode("md:NamePrefix", $cfgNs) } else { $null } +$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "Расш_" } + +# --- Map ModulePath to file path --- +# ModulePath formats: +# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl +# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl +# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl +# CommonModule.X -> CommonModules/X/Ext/Module.bsl +# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl +# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl +# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl + +$typeDirMap = @{ + "Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums" + "CommonModule"="CommonModules"; "Report"="Reports"; "DataProcessor"="DataProcessors" + "ExchangePlan"="ExchangePlans"; "ChartOfAccounts"="ChartsOfAccounts" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" + "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" + "AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters" +} + +$parts = $ModulePath.Split(".") +if ($parts.Count -lt 2) { + Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module or CommonModule.Name" + exit 1 +} + +$objType = $parts[0] +$objName = $parts[1] + +if (-not $typeDirMap.ContainsKey($objType)) { + Write-Error "Unknown object type: $objType" + exit 1 +} +$dirName = $typeDirMap[$objType] + +$bslFile = $null +if ($objType -eq "CommonModule") { + # CommonModule.X -> CommonModules/X/Ext/Module.bsl + $bslFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Ext") "Module.bsl" +} elseif ($parts.Count -ge 4 -and $parts[2] -eq "Form") { + # Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl + $formName = $parts[3] + $bslFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext") "Form") "Module.bsl" +} elseif ($parts.Count -ge 3) { + # Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl + $moduleName = $parts[2] + $moduleFileName = switch ($moduleName) { + "ObjectModule" { "ObjectModule.bsl" } + "ManagerModule" { "ManagerModule.bsl" } + "RecordSetModule" { "RecordSetModule.bsl" } + "CommandModule" { "CommandModule.bsl" } + default { "$moduleName.bsl" } + } + $bslFile = Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) (Join-Path "Ext" $moduleFileName) +} else { + Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name" + exit 1 +} + +# --- Map InterceptorType to decorator --- +$decorator = switch ($InterceptorType) { + "Before" { "&Перед" } + "After" { "&После" } + "ModificationAndControl" { "&ИзменениеИКонтроль" } +} + +# --- Map Context to annotation --- +$contextAnnotation = switch ($Context) { + "НаСервере" { "&НаСервере" } + "НаКлиенте" { "&НаКлиенте" } + "НаСервереБезКонтекста" { "&НаСервереБезКонтекста" } + default { "&$Context" } +} + +# --- Procedure name --- +$procName = "${namePrefix}${MethodName}" + +# --- Generate BSL code --- +$keyword = if ($IsFunction) { "Функция" } else { "Процедура" } +$endKeyword = if ($IsFunction) { "КонецФункции" } else { "КонецПроцедуры" } + +$bodyLines = @() +switch ($InterceptorType) { + "Before" { + $bodyLines += "`t// TODO: код перед вызовом оригинального метода" + } + "After" { + $bodyLines += "`t// TODO: код после вызова оригинального метода" + } + "ModificationAndControl" { + $bodyLines += "`t// Скопируйте тело оригинального метода и внесите изменения," + $bodyLines += "`t// используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки" + } +} + +if ($IsFunction) { + $bodyLines += "`t" + $bodyLines += "`tВозврат Неопределено; // TODO: заменить на реальное возвращаемое значение" +} + +$bslCode = @() +$bslCode += "$contextAnnotation" +$bslCode += "${decorator}(`"$MethodName`")" +$bslCode += "$keyword ${procName}()" +$bslCode += $bodyLines +$bslCode += "$endKeyword" + +$bslText = ($bslCode -join "`r`n") + "`r`n" + +# --- Check if file exists and append --- +$bslDir = Split-Path $bslFile -Parent +if (-not (Test-Path $bslDir)) { + New-Item -ItemType Directory -Path $bslDir -Force | Out-Null +} + +$enc = New-Object System.Text.UTF8Encoding($true) + +if (Test-Path $bslFile) { + # Append to existing file + $existing = [System.IO.File]::ReadAllText($bslFile, $enc) + $separator = "`r`n" + if ($existing -and -not $existing.EndsWith("`n")) { + $separator = "`r`n`r`n" + } + $newContent = $existing + $separator + $bslText + [System.IO.File]::WriteAllText($bslFile, $newContent, $enc) + Write-Host "[OK] Добавлен перехватчик в существующий файл" +} else { + [System.IO.File]::WriteAllText($bslFile, $bslText, $enc) + Write-Host "[OK] Создан файл модуля" +} + +Write-Host " Файл: $bslFile" +Write-Host " Декоратор: $decorator(`"$MethodName`")" +Write-Host " Процедура: ${procName}()" +Write-Host " Контекст: $contextAnnotation" diff --git a/.claude/skills/cfe-validate/SKILL.md b/.claude/skills/cfe-validate/SKILL.md new file mode 100644 index 00000000..46d81b34 --- /dev/null +++ b/.claude/skills/cfe-validate/SKILL.md @@ -0,0 +1,15 @@ +--- +name: cfe-validate +description: Валидация структурной корректности расширения конфигурации 1С (CFE) — корневая структура, свойства, состав, заимствованные объекты +argument-hint: [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +Валидирует расширение конфигурации 1С: XML-структура, свойства, ChildObjects, заимствованные объекты. + +```powershell +powershell.exe -NoProfile -File .claude\skills\cfe-validate\scripts\cfe-validate.ps1 -ExtensionPath src +``` diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 new file mode 100644 index 00000000..ab03fdd9 --- /dev/null +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 @@ -0,0 +1,607 @@ +# cfe-validate v1.0 — Validate 1C configuration extension structure (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ExtensionPath, + + [int]$MaxErrors = 30, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} + +if (Test-Path $ExtensionPath -PathType Container) { + $candidate = Join-Path $ExtensionPath "Configuration.xml" + if (Test-Path $candidate) { + $ExtensionPath = $candidate + } else { + Write-Host "[ERROR] No Configuration.xml found in directory: $ExtensionPath" + exit 1 + } +} + +if (-not (Test-Path $ExtensionPath)) { + Write-Host "[ERROR] File not found: $ExtensionPath" + exit 1 +} + +$resolvedPath = (Resolve-Path $ExtensionPath).Path +$configDir = 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_]*$' + +# 7 fixed ClassIds for Configuration +$validClassIds = @( + "9cd510cd-abfc-11d4-9434-004095e12fc7", + "9fcd25a0-4822-11d4-9414-008048da11f9", + "e3687481-0a87-462c-a166-9f34594f9bba", + "9de14907-ec23-4a07-96f0-85521cb6b53b", + "51f2d5d8-ea4d-4064-8892-82951750031e", + "e68182ea-4237-4383-967f-90c1e3370bc7", + "fb282519-d103-4dd3-bc12-cb271d631dfc" +) + +# 44 types in canonical order +$childObjectTypes = @( + "Language","Subsystem","StyleItem","Style", + "CommonPicture","SessionParameter","Role","CommonTemplate", + "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", + "XDTOPackage","WebService","HTTPService","WSReference", + "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", + "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", + "Constant","CommonForm","Catalog","Document", + "DocumentNumerator","Sequence","DocumentJournal","Enum", + "Report","DataProcessor","InformationRegister","AccumulationRegister", + "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", + "ChartOfCalculationTypes","CalculationRegister", + "BusinessProcess","Task","IntegrationService" +) + +# Type -> directory mapping +$childTypeDirMap = @{ + "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" + "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles" + "CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules" + "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages" + "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences" + "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" + "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions" + "FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes" + "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"; "Constant"="Constants" + "CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents" + "DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences" + "DocumentJournal"="DocumentJournals"; "Enum"="Enums"; "Report"="Reports" + "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters" + "AccumulationRegister"="AccumulationRegisters" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes" + "CalculationRegister"="CalculationRegisters" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" + "IntegrationService"="IntegrationServices" +} + +# Valid enum values for extension properties +$validEnumValues = @{ + "ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28") + "DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto") + "ScriptVariant" = @("Russian","English") + "InterfaceCompatibilityMode" = @("Taxi","TaxiEnableVersion8_2","Version8_2") +} + +# --- 1. Parse XML --- +Out-Line "" + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) +} catch { + Out-Line "=== Validation: Extension (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 +$expectedNs = "http://v8.1c.ru/8.3/MDClasses" + +if ($root.LocalName -ne "MetaDataObject") { + Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" + & $finalize + exit 1 +} + +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)" +} + +# Must have Configuration child +$cfgNode = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration" -and $child.NamespaceURI -eq $expectedNs) { + $cfgNode = $child; break + } +} + +if (-not $cfgNode) { + Report-Error "1. No element found inside MetaDataObject" + & $finalize + exit 1 +} + +# UUID +$cfgUuid = $cfgNode.GetAttribute("uuid") +if (-not $cfgUuid) { + Report-Error "1. Missing uuid on " + $check1Ok = $false +} elseif ($cfgUuid -notmatch $guidPattern) { + Report-Error "1. Invalid uuid '$cfgUuid' on " + $check1Ok = $false +} + +# Get name early for header +$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns) +$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } +$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } + +$script:output.Insert(0, "=== Validation: Extension.$objName ===$([Environment]::NewLine)") | Out-Null + +if ($check1Ok) { + Report-OK "1. Root structure: MetaDataObject/Configuration, version $version" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 2: InternalInfo --- +$internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns) +$check2Ok = $true + +if (-not $internalInfo) { + Report-Error "2. InternalInfo: missing" +} else { + $contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns) + if ($contained.Count -ne 7) { + Report-Warn "2. InternalInfo: expected 7 ContainedObject, found $($contained.Count)" + } + + $foundClassIds = @{} + foreach ($co in $contained) { + $classId = $co.SelectSingleNode("xr:ClassId", $ns) + $objectId = $co.SelectSingleNode("xr:ObjectId", $ns) + + if (-not $classId -or -not $classId.InnerText) { + Report-Error "2. ContainedObject missing ClassId" + $check2Ok = $false + continue + } + + $cid = $classId.InnerText + if ($validClassIds -notcontains $cid) { + Report-Error "2. Unknown ClassId: $cid" + $check2Ok = $false + } + + if ($foundClassIds.ContainsKey($cid)) { + Report-Error "2. Duplicate ClassId: $cid" + $check2Ok = $false + } + $foundClassIds[$cid] = $true + + if (-not $objectId -or -not $objectId.InnerText) { + Report-Error "2. ContainedObject missing ObjectId for ClassId $cid" + $check2Ok = $false + } elseif ($objectId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ObjectId '$($objectId.InnerText)' for ClassId $cid" + $check2Ok = $false + } + } + + $missingIds = @($validClassIds | Where-Object { -not $foundClassIds.ContainsKey($_) }) + if ($missingIds.Count -gt 0) { + Report-Warn "2. Missing ClassIds: $($missingIds.Count) of 7" + } + + if ($check2Ok) { + Report-OK "2. InternalInfo: $($contained.Count) ContainedObject, all ClassIds valid" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 3: Extension-specific properties --- +if (-not $propsNode) { + Report-Error "3. Properties block missing" +} else { + $check3Ok = $true + + # ObjectBelonging = Adopted + $obNode = $propsNode.SelectSingleNode("md:ObjectBelonging", $ns) + if (-not $obNode -or $obNode.InnerText -ne "Adopted") { + Report-Error "3. ObjectBelonging must be 'Adopted', got '$($obNode.InnerText)'" + $check3Ok = $false + } + + # Name + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "3. Name is missing or empty" + $check3Ok = $false + } else { + $nameVal = $nameNode.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "3. Name '$nameVal' is not a valid 1C identifier" + $check3Ok = $false + } + } + + # ConfigurationExtensionPurpose + $purposeNode = $propsNode.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns) + $validPurposes = @("Patch","Customization","AddOn") + if (-not $purposeNode -or -not $purposeNode.InnerText) { + Report-Error "3. ConfigurationExtensionPurpose is missing" + $check3Ok = $false + } elseif ($validPurposes -notcontains $purposeNode.InnerText) { + Report-Error "3. ConfigurationExtensionPurpose '$($purposeNode.InnerText)' invalid (expected: Patch, Customization, AddOn)" + $check3Ok = $false + } + + # NamePrefix + $prefixNode = $propsNode.SelectSingleNode("md:NamePrefix", $ns) + if (-not $prefixNode -or -not $prefixNode.InnerText) { + Report-Warn "3. NamePrefix is empty" + } + + # KeepMappingToExtendedConfigurationObjectsByIDs + $keepMapNode = $propsNode.SelectSingleNode("md:KeepMappingToExtendedConfigurationObjectsByIDs", $ns) + if (-not $keepMapNode) { + Report-Warn "3. KeepMappingToExtendedConfigurationObjectsByIDs is missing" + } + + # DefaultLanguage + $defLangNode = $propsNode.SelectSingleNode("md:DefaultLanguage", $ns) + $defLang = if ($defLangNode -and $defLangNode.InnerText) { $defLangNode.InnerText } else { "" } + + if ($check3Ok) { + $purposeVal = if ($purposeNode) { $purposeNode.InnerText } else { "?" } + $prefixVal = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "(empty)" } + Report-OK "3. Extension properties: Name=`"$objName`", Purpose=$purposeVal, Prefix=$prefixVal" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 4: Enum property values --- +if ($propsNode) { + $enumChecked = 0 + $check4Ok = $true + + foreach ($propName in $validEnumValues.Keys) { + $propNode = $propsNode.SelectSingleNode("md:$propName", $ns) + if ($propNode -and $propNode.InnerText) { + $val = $propNode.InnerText + $allowed = $validEnumValues[$propName] + if ($allowed -notcontains $val) { + Report-Error "4. Property '$propName' has invalid value '$val'" + $check4Ok = $false + } + $enumChecked++ + } + } + + if ($check4Ok) { + Report-OK "4. Property values: $enumChecked enum properties checked" + } +} else { + Report-Warn "4. No Properties block to check" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 5: ChildObjects — valid types, no duplicates, order --- +$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) + +if (-not $childObjNode) { + Report-Error "5. ChildObjects block missing" +} else { + $check5Ok = $true + $totalCount = 0 + $typeCounts = @{} + $duplicates = @{} + $typeFirstIndex = @{} + $lastTypeOrder = -1 + $orderOk = $true + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + $objNameVal = $child.InnerText + + $typeIdx = $childObjectTypes.IndexOf($typeName) + if ($typeIdx -lt 0) { + Report-Error "5. Unknown type '$typeName' in ChildObjects" + $check5Ok = $false + } else { + if (-not $typeFirstIndex.ContainsKey($typeName)) { + $typeFirstIndex[$typeName] = $typeIdx + if ($typeIdx -lt $lastTypeOrder) { + Report-Warn "5. Type '$typeName' is out of canonical order (after type at position $lastTypeOrder)" + $orderOk = $false + } + $lastTypeOrder = $typeIdx + } + } + + if (-not $typeCounts.ContainsKey($typeName)) { $typeCounts[$typeName] = @{} } + if ($typeCounts[$typeName].ContainsKey($objNameVal)) { + if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) { + Report-Error "5. Duplicate: $typeName.$objNameVal" + $duplicates["$typeName.$objNameVal"] = $true + $check5Ok = $false + } + } else { + $typeCounts[$typeName][$objNameVal] = $true + } + + $totalCount++ + } + + $typeCount = $typeCounts.Count + if ($check5Ok) { + $orderInfo = if ($orderOk) { ", order correct" } else { "" } + Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 6: DefaultLanguage references existing Language in ChildObjects --- +if ($defLang -and $childObjNode) { + $langName = $defLang + if ($langName.StartsWith("Language.")) { + $langName = $langName.Substring(9) + } + + $found = $false + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language" -and $child.InnerText -eq $langName) { + $found = $true; break + } + } + + if ($found) { + Report-OK "6. DefaultLanguage `"$defLang`" found in ChildObjects" + } else { + Report-Error "6. DefaultLanguage `"$defLang`" not found in ChildObjects" + } +} else { + if (-not $defLang) { + Report-Warn "6. Cannot check DefaultLanguage (empty)" + } else { + Report-Warn "6. Cannot check DefaultLanguage (no ChildObjects)" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 7: Language files exist --- +if ($childObjNode) { + $langNames = @() + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language") { + $langNames += $child.InnerText + } + } + + if ($langNames.Count -gt 0) { + $existCount = 0 + foreach ($ln in $langNames) { + $langFile = Join-Path (Join-Path $configDir "Languages") "$ln.xml" + if (Test-Path $langFile) { + $existCount++ + } else { + Report-Warn "7. Language file missing: Languages/$ln.xml" + } + } + if ($existCount -eq $langNames.Count) { + Report-OK "7. Language files: $existCount/$($langNames.Count) exist" + } + } else { + Report-Warn "7. No Language entries in ChildObjects" + } +} else { + Report-Warn "7. Cannot check language files (no ChildObjects)" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 8: Object directories exist --- +if ($childObjNode) { + $dirsToCheck = @{} + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + if ($typeName -eq "Language") { continue } + if ($childTypeDirMap.ContainsKey($typeName)) { + $dirName = $childTypeDirMap[$typeName] + if (-not $dirsToCheck.ContainsKey($dirName)) { + $dirsToCheck[$dirName] = 0 + } + $dirsToCheck[$dirName] = $dirsToCheck[$dirName] + 1 + } + } + + $missingDirs = @() + foreach ($dir in $dirsToCheck.Keys) { + $dirPath = Join-Path $configDir $dir + if (-not (Test-Path $dirPath -PathType Container)) { + $missingDirs += "$dir ($($dirsToCheck[$dir]) objects)" + } + } + + if ($missingDirs.Count -eq 0) { + Report-OK "8. Object directories: $($dirsToCheck.Count) directories, all exist" + } else { + foreach ($md in $missingDirs) { + Report-Warn "8. Missing directory: $md" + } + } +} else { + Report-OK "8. Object directories: N/A" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 9: Borrowed objects validation --- +if ($childObjNode) { + $borrowedCount = 0 + $borrowedOk = 0 + $check9Ok = $true + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + $childName = $child.InnerText + if ($typeName -eq "Language") { continue } + + if (-not $childTypeDirMap.ContainsKey($typeName)) { continue } + $dirName = $childTypeDirMap[$typeName] + $objFile = Join-Path (Join-Path $configDir $dirName) "$childName.xml" + + if (-not (Test-Path $objFile)) { continue } + + # Parse object XML + $objDoc = $null + try { + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $false + $objDoc.Load($objFile) + } catch { + Report-Warn "9. Cannot parse $dirName/$childName.xml: $($_.Exception.Message)" + continue + } + + $objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $objNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + + # Find the object element (Catalog, Document, etc.) + $objRoot = $objDoc.DocumentElement + $objEl = $null + foreach ($c in $objRoot.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { continue } + + $objProps = $objEl.SelectSingleNode("md:Properties", $objNs) + if (-not $objProps) { continue } + + $obNode = $objProps.SelectSingleNode("md:ObjectBelonging", $objNs) + if ($obNode -and $obNode.InnerText -eq "Adopted") { + $borrowedCount++ + + # Check ExtendedConfigurationObject + $extObj = $objProps.SelectSingleNode("md:ExtendedConfigurationObject", $objNs) + if (-not $extObj -or -not $extObj.InnerText) { + Report-Error "9. Borrowed ${typeName}.${childName}: missing ExtendedConfigurationObject" + $check9Ok = $false + } elseif ($extObj.InnerText -notmatch $guidPattern) { + Report-Error "9. Borrowed ${typeName}.${childName}: invalid ExtendedConfigurationObject UUID '$($extObj.InnerText)'" + $check9Ok = $false + } else { + $borrowedOk++ + } + } + + if ($script:stopped) { break } + } + + if ($borrowedCount -eq 0) { + Report-OK "9. Borrowed objects: none found" + } elseif ($check9Ok) { + Report-OK "9. Borrowed objects: $borrowedOk/$borrowedCount validated" + } +} + +# --- Final output --- +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0 diff --git a/README.md b/README.md index c565b145..41eb300f 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ | Схема компоновки (СКД) | 4 навыка `/skd-*` | Анализ, генерация из JSON DSL, точечное редактирование, валидация схем компоновки данных | [Подробнее](docs/skd-guide.md) | | Метаданные конфигурации | 4 навыка `/meta-*` | Создание, анализ, редактирование, валидация объектов метаданных (23 типа) | [Подробнее](docs/meta-guide.md) | | Корневая конфигурация | 4 навыка `/cf-*` | Создание, анализ, редактирование, валидация корневых файлов конфигурации | [Подробнее](docs/cf-guide.md) | +| Расширения (CFE) | 5 навыков `/cfe-*` | Создание, заимствование, перехват методов, валидация, анализ расширений | [Подробнее](docs/cfe-guide.md) | | Подсистемы (Subsystem) | 4 навыка `/subsystem-*` | Анализ, создание, редактирование, валидация подсистем конфигурации | — | | Командный интерфейс (CI) | 2 навыка `/interface-*` | Редактирование и валидация CommandInterface.xml подсистем | — | | Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — | @@ -56,6 +57,7 @@ - [Объекты конфигурации](docs/1c-config-objects-spec.md) — XML-формат объектов метаданных конфигурации (23 типа) - [Подсистемы и командный интерфейс](docs/1c-subsystem-spec.md) — XML-формат подсистем, CommandInterface.xml, секции видимости/размещения/порядка - [Корневая конфигурация](docs/1c-configuration-spec.md) — XML-формат Configuration.xml, ConfigDumpInfo.xml, Languages/, 44 типа ChildObjects +- [Расширения конфигурации (CFE)](docs/1c-extension-spec.md) — XML-формат выгрузки расширений конфигурации ## Структура репозитория @@ -99,6 +101,11 @@ ├── cf-init/ # Создание пустой конфигурации ├── cf-edit/ # Редактирование конфигурации ├── cf-validate/ # Валидация конфигурации +├── cfe-init/ # Создание расширения +├── cfe-borrow/ # Заимствование объектов +├── cfe-patch-method/ # Перехват методов +├── cfe-validate/ # Валидация расширения +├── cfe-diff/ # Анализ и сравнение ├── subsystem-info/ # Анализ структуры подсистемы ├── subsystem-compile/ # Создание подсистемы из JSON ├── subsystem-edit/ # Редактирование подсистемы @@ -114,6 +121,7 @@ docs/ ├── skd-guide.md # Гайд: схема компоновки данных ├── meta-guide.md # Гайд: объекты метаданных конфигурации ├── cf-guide.md # Гайд: корневые файлы конфигурации +├── cfe-guide.md # Гайд: расширения конфигурации (CFE) ├── 1c-epf-spec.md # Спецификация XML-формата (EPF) ├── 1c-erf-spec.md # Спецификация XML-формата (ERF) ├── 1c-form-spec.md # Спецификация управляемых форм @@ -128,5 +136,6 @@ docs/ ├── 1c-dcs-spec.md # Спецификация СКД (DataCompositionSchema) ├── skd-dsl-spec.md # Спецификация SKD DSL ├── role-dsl-spec.md # Спецификация Role DSL +├── 1c-extension-spec.md # Спецификация расширений конфигурации (CFE) └── 1c-subsystem-spec.md # Спецификация подсистем и командного интерфейса ``` diff --git a/docs/cfe-guide.md b/docs/cfe-guide.md new file mode 100644 index 00000000..32a61bfa --- /dev/null +++ b/docs/cfe-guide.md @@ -0,0 +1,175 @@ +# Расширения конфигурации (CFE) + +Навыки группы `/cfe-*` позволяют создавать, заимствовать объекты, перехватывать методы, проверять и анализировать расширения конфигурации 1С. + +## Навыки + +| Навык | Параметры | Описание | +|-------|-----------|----------| +| `/cfe-init` | ` [-Purpose Patch\|Customization\|AddOn] [-CompatibilityMode]` | Создание расширения (scaffold XML-исходников) | +| `/cfe-borrow` | `-ExtensionPath -ConfigPath -Object "Type.Name"` | Заимствование объектов из конфигурации | +| `/cfe-patch-method` | `-ExtensionPath -ModulePath "Type.Name.Module" -MethodName "X" -InterceptorType Before` | Генерация перехватчика метода | +| `/cfe-validate` | ` [-MaxErrors 30]` | Валидация структурной корректности (9 проверок) | +| `/cfe-diff` | `-ExtensionPath -ConfigPath [-Mode A\|B]` | Анализ расширения и проверка переноса | + +## Рабочий цикл + +``` +cf-info (версия, совместимость) + ↓ +/cfe-init → scaffold расширения + ↓ +/cfe-borrow → заимствование объектов из конфигурации + ↓ +/cfe-patch-method → перехват методов + ↓ +/cfe-validate → проверка корректности + ↓ +/cfe-diff Mode A → обзор изменений +``` + +## Типичные сценарии + +### Создание расширения для исправления бага + +``` +> Создай расширение для исправления бага в справочнике Контрагенты, + конфигурация ERP в C:\cfsrc\erp +``` + +Claude выполнит: +1. `/cf-info C:\cfsrc\erp -Mode brief` — получить версию и режим совместимости +2. `/cfe-init` — создать расширение с нужным `CompatibilityMode` +3. `/cfe-borrow` — заимствовать `Catalog.Контрагенты` +4. `/cfe-patch-method` — создать перехватчик нужного метода +5. `/cfe-validate` — проверить результат + +### Анализ существующего расширения + +``` +> Покажи что изменено в расширении src/ +``` + +Claude вызовет `/cfe-diff -Mode A` и покажет: заимствованные объекты, перехватчики, собственные объекты. + +### Проверка переноса изменений + +``` +> Проверь, все ли изменения из расширения перенесены в конфигурацию +``` + +Claude вызовет `/cfe-diff -Mode B` — найдёт блоки `#Вставка` и проверит их наличие в конфигурации. + +## cfe-init — создание расширения + +Параметры: + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `Name` | Имя расширения (обязат.) | — | +| `Synonym` | Синоним | = Name | +| `NamePrefix` | Префикс собственных объектов | = Name + "_" | +| `OutputDir` | Каталог | `src` | +| `Purpose` | Назначение | `Customization` | +| `Version` | Версия | — | +| `Vendor` | Поставщик | — | +| `CompatibilityMode` | Режим совместимости | `Version8_3_24` | +| `NoRole` | Без основной роли | false | + +Создаёт: +``` +/ +├── Configuration.xml # Свойства расширения +├── Languages/ +│ └── Русский.xml # Язык (Adopted) +└── Roles/ # Если не -NoRole + └── ОсновнаяРоль.xml +``` + +Назначение расширения (`Purpose`): +- `Patch` — исправление ошибок (минимальные изменения, только перехватчики) +- `Customization` — доработка (реквизиты, формы, модули) +- `AddOn` — дополнение (полноценный функционал) + +## cfe-borrow — заимствование объектов + +Заимствует объекты из основной конфигурации в расширение. Создаёт минимальные XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`. + +Формат `-Object`: +- `Catalog.Контрагенты` — справочник +- `CommonModule.РаботаСФайлами` — общий модуль +- `Enum.ВидыОплат` — перечисление +- `Document.Заказ ;; Catalog.Товары` — несколько объектов через `;;` + +Поддерживаемые типы: Catalog, Document, Enum, CommonModule, Report, DataProcessor, ExchangePlan, InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task, и другие (44 типа). + +## cfe-patch-method — перехват методов + +Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта. + +Параметры: + +| Параметр | Описание | +|----------|----------| +| `ModulePath` | `Catalog.X.ObjectModule`, `CommonModule.Y`, `Catalog.X.Form.Z` | +| `MethodName` | Имя перехватываемого метода | +| `InterceptorType` | `Before` / `After` / `ModificationAndControl` | +| `Context` | `НаСервере` / `НаКлиенте` / `НаСервереБезКонтекста` | +| `IsFunction` | Добавить `Возврат` | + +Типы перехватчиков: + +| Тип | Декоратор | Когда использовать | +|-----|-----------|-------------------| +| `Before` | `&Перед` | Выполнить код до вызова оригинального метода | +| `After` | `&После` | Выполнить код после вызова оригинального метода | +| `ModificationAndControl` | `&ИзменениеИКонтроль` | Полная замена тела метода с маркерами `#Вставка`/`#Удаление` | + +Пример генерируемого кода (`Before`): +```bsl +&НаСервере +&Перед("ПриЗаписи") +Процедура Расш1_ПриЗаписи() + // TODO: код перед вызовом оригинального метода +КонецПроцедуры +``` + +## cfe-validate — проверки + +| # | Проверка | Уровень | +|---|----------|---------| +| 1 | XML well-formedness, MetaDataObject/Configuration, version | ERROR | +| 2 | InternalInfo: 7 ContainedObject, валидные ClassId | ERROR | +| 3 | Extension properties: ObjectBelonging=Adopted, Name, Purpose, NamePrefix, KeepMapping | ERROR | +| 4 | Enum-значения (4 свойства) | ERROR | +| 5 | ChildObjects: валидные типы, нет дубликатов, порядок | ERROR/WARN | +| 6 | DefaultLanguage ссылается на существующий Language | ERROR | +| 7 | Файлы языков существуют | WARN | +| 8 | Каталоги объектов существуют | WARN | +| 9 | Заимствованные объекты: ObjectBelonging=Adopted, ExtendedConfigurationObject UUID | ERROR/WARN | + +## cfe-diff — режимы + +### Mode A — обзор расширения + +Для каждого объекта показывает: +- `[BORROWED]` — заимствованный: перехватчики, собственные реквизиты/формы +- `[OWN]` — собственный: количество реквизитов, ТЧ, форм + +### Mode B — проверка переноса + +Для каждого `&ИзменениеИКонтроль` проверяет, перенесены ли блоки `#Вставка` в конфигурацию: +- `[TRANSFERRED]` — код найден в конфигурации +- `[NOT_TRANSFERRED]` — код не найден +- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден + +## Связь с другими навыками + +- `/cf-info` — получение версии и совместимости конфигурации перед `cfe-init` +- `/meta-compile` — создание собственных объектов расширения (реквизиты, ТЧ) +- `/form-compile`, `/form-edit` — создание и модификация форм расширения +- `/cfe-validate` — всегда проверяйте расширение после изменений + +## Спецификации + +- [1c-extension-spec.md](1c-extension-spec.md) — XML-формат выгрузки расширений конфигурации (CFE)