diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1
index 3c227d7b..2bc825e0 100644
--- a/.claude/skills/meta-compile/scripts/meta-compile.ps1
+++ b/.claude/skills/meta-compile/scripts/meta-compile.ps1
@@ -1,4 +1,4 @@
-# meta-compile v1.12 — Compile 1C metadata object from JSON
+# meta-compile v1.13 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -21,6 +21,114 @@ if (-not (Test-Path $JsonPath)) {
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
$def = $json | ConvertFrom-Json
+# --- Support guard (Ext/ParentConfigurations.bin) ---
+# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
+# read-only configs unless allowed. Trigger = bin present; reaction from
+# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
+# throws — guard errors degrade to allow.
+function Get-RootUuid([string]$xmlPath) {
+ if (-not (Test-Path $xmlPath)) { return $null }
+ try {
+ [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
+ $el = $mx.DocumentElement.FirstChild
+ while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
+ if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
+ } catch {}
+ return $null
+}
+function Find-V8Project([string]$startDir) {
+ $d = $startDir
+ for ($i = 0; $i -lt 20 -and $d; $i++) {
+ $pj = Join-Path $d ".v8-project.json"
+ if (Test-Path $pj) { return $pj }
+ $parent = [System.IO.Path]::GetDirectoryName($d)
+ if ($parent -eq $d) { break }
+ $d = $parent
+ }
+ return $null
+}
+function Get-EditMode([string]$cfgDir) {
+ try {
+ $pj = Find-V8Project (Get-Location).Path
+ if (-not $pj) { $pj = Find-V8Project $cfgDir }
+ if (-not $pj) { return 'deny' }
+ $proj = Get-Content -Raw $pj | ConvertFrom-Json
+ $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
+ if ($proj.databases) {
+ foreach ($db in $proj.databases) {
+ if ($db.configSrc) {
+ $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
+ if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
+ if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
+ }
+ }
+ }
+ }
+ if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
+ return 'deny'
+ } catch { return 'deny' }
+}
+function Assert-EditAllowed([string]$targetPath, [string]$require) {
+ try {
+ $rp = $targetPath
+ try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
+ $elemUuid = Get-RootUuid $rp
+ $cfgDir = $null; $binPath = $null
+ $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
+ for ($i = 0; $i -lt 12 -and $d; $i++) {
+ if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
+ if (-not $cfgDir) {
+ $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
+ if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
+ }
+ if ($elemUuid -and $cfgDir) { break }
+ $parent = [System.IO.Path]::GetDirectoryName($d)
+ if ($parent -eq $d) { break }
+ $d = $parent
+ }
+ # New object (no element file): fall back to config root uuid.
+ if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
+ if (-not $binPath -or -not (Test-Path $binPath)) { return }
+ $bytes = [System.IO.File]::ReadAllBytes($binPath)
+ if ($bytes.Length -le 32) { return }
+ $start = 0
+ if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
+ $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
+ $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
+ if (-not $hm.Success) { return }
+ $G = [int]$hm.Groups[1].Value
+ $K = [int]$hm.Groups[2].Value
+ if ($K -eq 0) { return }
+ $best = $null
+ if ($elemUuid) {
+ $u = [regex]::Escape($elemUuid.ToLower())
+ foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
+ $f1 = [int]$m.Groups[1].Value
+ if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
+ }
+ }
+ $blocked = $false; $reason = ""
+ if ($G -eq 1) { $blocked = $true; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
+ elseif ($require -eq 'removed') {
+ if ($null -ne $best -and $best -ne 2) { $blocked = $true; $reason = "объект на поддержке (не снят с поддержки) — удаление сломает обновления" }
+ }
+ else {
+ if ($null -ne $best -and $best -eq 0) { $blocked = $true; $reason = "объект на замке (поддержка поставщика) — прямая правка сломает обновления" }
+ }
+ if (-not $blocked) { return }
+ $mode = Get-EditMode $cfgDir
+ if ($mode -eq 'off') { return }
+ # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
+ # latter throws and would be swallowed by this function's own catch.
+ $msg = "[support-guard] Операция запрещена: $reason.`n Цель: $rp`n Безопасные пути: доработка через расширение (cfe-*); включить редактирование объекта/корня в конфигураторе; снять с поддержки.`n Отключить проверку: editingAllowedCheck = warn|off в .v8-project.json."
+ if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
+ [Console]::Error.WriteLine($msg)
+ exit 1
+ } catch { return }
+}
+
+Assert-EditAllowed $OutputDir 'editable'
+
# --- Batch mode: JSON array of objects ---
if ($def -is [array] -or ($null -ne $def -and $def.GetType().BaseType.Name -eq 'Array')) {
$batchOk = 0
diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py
index 6e8a07a4..13c3c488 100644
--- a/.claude/skills/meta-compile/scripts/meta-compile.py
+++ b/.claude/skills/meta-compile/scripts/meta-compile.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# meta-compile v1.12 — Compile 1C metadata object from JSON
+# meta-compile v1.13 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -11,10 +11,146 @@ import sys
import tempfile
import uuid
import xml.etree.ElementTree as ET
+from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
+# ============================================================
+# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
+# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
+# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
+# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
+# ============================================================
+
+def _sg_root_uuid(xml_path):
+ if not os.path.isfile(xml_path):
+ return None
+ try:
+ mx = etree.parse(xml_path).getroot()
+ for child in mx:
+ if isinstance(child.tag, str) and child.get("uuid"):
+ return child.get("uuid")
+ except Exception:
+ return None
+ return None
+
+
+def _sg_find_v8project(start_dir):
+ d = start_dir
+ for _ in range(20):
+ if not d:
+ break
+ pj = os.path.join(d, ".v8-project.json")
+ if os.path.isfile(pj):
+ return pj
+ parent = os.path.dirname(d)
+ if parent == d:
+ break
+ d = parent
+ return None
+
+
+def _sg_get_edit_mode(cfg_dir):
+ try:
+ pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
+ if not pj:
+ return "deny"
+ proj = json.loads(open(pj, encoding="utf-8-sig").read())
+ cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
+ for db in proj.get("databases", []):
+ src = db.get("configSrc")
+ if src:
+ src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
+ if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
+ if db.get("editingAllowedCheck"):
+ return db["editingAllowedCheck"]
+ if proj.get("editingAllowedCheck"):
+ return proj["editingAllowedCheck"]
+ return "deny"
+ except Exception:
+ return "deny"
+
+
+def assert_edit_allowed(target_path, require):
+ try:
+ rp = os.path.abspath(target_path)
+ elem_uuid = _sg_root_uuid(rp)
+ cfg_dir = None
+ bin_path = None
+ d = rp if os.path.isdir(rp) else os.path.dirname(rp)
+ for _ in range(12):
+ if not d:
+ break
+ if not elem_uuid:
+ elem_uuid = _sg_root_uuid(d + ".xml")
+ if not cfg_dir:
+ cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
+ if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
+ cfg_dir = d
+ bin_path = cand
+ if elem_uuid and cfg_dir:
+ break
+ parent = os.path.dirname(d)
+ if parent == d:
+ break
+ d = parent
+ if not elem_uuid and cfg_dir:
+ elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
+ if not bin_path or not os.path.exists(bin_path):
+ return
+ data = open(bin_path, "rb").read()
+ if len(data) <= 32:
+ return
+ if data[:3] == b"\xef\xbb\xbf":
+ data = data[3:]
+ text = data.decode("utf-8", "replace")
+ h = re.match(r"\{6,(\d+),(\d+),", text)
+ if not h:
+ return
+ g = int(h.group(1))
+ k = int(h.group(2))
+ if k == 0:
+ return
+ best = None
+ if elem_uuid:
+ for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
+ f1 = int(m.group(1))
+ if best is None or f1 < best:
+ best = f1
+ blocked = False
+ reason = ""
+ if g == 1:
+ blocked = True
+ reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
+ elif require == "removed":
+ if best is not None and best != 2:
+ blocked = True
+ reason = "объект на поддержке (не снят с поддержки) — удаление сломает обновления"
+ else:
+ if best is not None and best == 0:
+ blocked = True
+ reason = "объект на замке (поддержка поставщика) — прямая правка сломает обновления"
+ if not blocked:
+ return
+ mode = _sg_get_edit_mode(cfg_dir)
+ if mode == "off":
+ return
+ if mode == "warn":
+ sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
+ return
+ sys.stderr.write(
+ f"[support-guard] Операция запрещена: {reason}.\n"
+ f" Цель: {rp}\n"
+ f" Безопасные пути: доработка через расширение (cfe-*); включить редактирование объекта/корня в конфигураторе; снять с поддержки.\n"
+ f" Отключить проверку: editingAllowedCheck = warn|off в .v8-project.json.\n"
+ )
+ sys.exit(1)
+ except SystemExit:
+ raise
+ except Exception:
+ return
+
# ---------------------------------------------------------------------------
# Inline utilities
# ---------------------------------------------------------------------------
@@ -83,6 +219,8 @@ with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = json.loads(json_text)
+assert_edit_allowed(output_dir, "editable")
+
# --- Batch mode: JSON array of objects ---
if isinstance(defn, list):
batch_ok = 0
diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1
index cdec281f..09f02573 100644
--- a/.claude/skills/meta-edit/scripts/meta-edit.ps1
+++ b/.claude/skills/meta-edit/scripts/meta-edit.ps1
@@ -1,4 +1,4 @@
-# meta-edit v1.6 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
+# meta-edit v1.7 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -152,6 +152,114 @@ if (-not (Test-Path $ObjectPath)) {
}
$resolvedPath = (Resolve-Path $ObjectPath).Path
+# --- Support guard (Ext/ParentConfigurations.bin) ---
+# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
+# read-only configs unless allowed. Trigger = bin present; reaction from
+# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
+# throws — guard errors degrade to allow.
+function Get-RootUuid([string]$xmlPath) {
+ if (-not (Test-Path $xmlPath)) { return $null }
+ try {
+ [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
+ $el = $mx.DocumentElement.FirstChild
+ while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
+ if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
+ } catch {}
+ return $null
+}
+function Find-V8Project([string]$startDir) {
+ $d = $startDir
+ for ($i = 0; $i -lt 20 -and $d; $i++) {
+ $pj = Join-Path $d ".v8-project.json"
+ if (Test-Path $pj) { return $pj }
+ $parent = [System.IO.Path]::GetDirectoryName($d)
+ if ($parent -eq $d) { break }
+ $d = $parent
+ }
+ return $null
+}
+function Get-EditMode([string]$cfgDir) {
+ try {
+ $pj = Find-V8Project (Get-Location).Path
+ if (-not $pj) { $pj = Find-V8Project $cfgDir }
+ if (-not $pj) { return 'deny' }
+ $proj = Get-Content -Raw $pj | ConvertFrom-Json
+ $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
+ if ($proj.databases) {
+ foreach ($db in $proj.databases) {
+ if ($db.configSrc) {
+ $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
+ if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
+ if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
+ }
+ }
+ }
+ }
+ if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
+ return 'deny'
+ } catch { return 'deny' }
+}
+function Assert-EditAllowed([string]$targetPath, [string]$require) {
+ try {
+ $rp = $targetPath
+ try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
+ $elemUuid = Get-RootUuid $rp
+ $cfgDir = $null; $binPath = $null
+ $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
+ for ($i = 0; $i -lt 12 -and $d; $i++) {
+ if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
+ if (-not $cfgDir) {
+ $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
+ if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
+ }
+ if ($elemUuid -and $cfgDir) { break }
+ $parent = [System.IO.Path]::GetDirectoryName($d)
+ if ($parent -eq $d) { break }
+ $d = $parent
+ }
+ # New object (no element file): fall back to config root uuid.
+ if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
+ if (-not $binPath -or -not (Test-Path $binPath)) { return }
+ $bytes = [System.IO.File]::ReadAllBytes($binPath)
+ if ($bytes.Length -le 32) { return }
+ $start = 0
+ if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
+ $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
+ $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
+ if (-not $hm.Success) { return }
+ $G = [int]$hm.Groups[1].Value
+ $K = [int]$hm.Groups[2].Value
+ if ($K -eq 0) { return }
+ $best = $null
+ if ($elemUuid) {
+ $u = [regex]::Escape($elemUuid.ToLower())
+ foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
+ $f1 = [int]$m.Groups[1].Value
+ if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
+ }
+ }
+ $blocked = $false; $reason = ""
+ if ($G -eq 1) { $blocked = $true; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
+ elseif ($require -eq 'removed') {
+ if ($null -ne $best -and $best -ne 2) { $blocked = $true; $reason = "объект на поддержке (не снят с поддержки) — удаление сломает обновления" }
+ }
+ else {
+ if ($null -ne $best -and $best -eq 0) { $blocked = $true; $reason = "объект на замке (поддержка поставщика) — прямая правка сломает обновления" }
+ }
+ if (-not $blocked) { return }
+ $mode = Get-EditMode $cfgDir
+ if ($mode -eq 'off') { return }
+ # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
+ # latter throws and would be swallowed by this function's own catch.
+ $msg = "[support-guard] Операция запрещена: $reason.`n Цель: $rp`n Безопасные пути: доработка через расширение (cfe-*); включить редактирование объекта/корня в конфигураторе; снять с поддержки.`n Отключить проверку: editingAllowedCheck = warn|off в .v8-project.json."
+ if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
+ [Console]::Error.WriteLine($msg)
+ exit 1
+ } catch { return }
+}
+
+Assert-EditAllowed $resolvedPath 'editable'
+
# --- Load XML ---
$script:xmlDoc = New-Object System.Xml.XmlDocument
$script:xmlDoc.PreserveWhitespace = $true
diff --git a/.claude/skills/meta-edit/scripts/meta-edit.py b/.claude/skills/meta-edit/scripts/meta-edit.py
index 53a9e942..b9115d88 100644
--- a/.claude/skills/meta-edit/scripts/meta-edit.py
+++ b/.claude/skills/meta-edit/scripts/meta-edit.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# meta-edit v1.6 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
+# meta-edit v1.7 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -11,6 +11,143 @@ import sys
import uuid
from lxml import etree
+
+# ============================================================
+# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
+# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
+# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
+# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
+# ============================================================
+
+def _sg_root_uuid(xml_path):
+ if not os.path.isfile(xml_path):
+ return None
+ try:
+ mx = etree.parse(xml_path).getroot()
+ for child in mx:
+ if isinstance(child.tag, str) and child.get("uuid"):
+ return child.get("uuid")
+ except Exception:
+ return None
+ return None
+
+
+def _sg_find_v8project(start_dir):
+ d = start_dir
+ for _ in range(20):
+ if not d:
+ break
+ pj = os.path.join(d, ".v8-project.json")
+ if os.path.isfile(pj):
+ return pj
+ parent = os.path.dirname(d)
+ if parent == d:
+ break
+ d = parent
+ return None
+
+
+def _sg_get_edit_mode(cfg_dir):
+ try:
+ pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
+ if not pj:
+ return "deny"
+ proj = json.loads(open(pj, encoding="utf-8-sig").read())
+ cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
+ for db in proj.get("databases", []):
+ src = db.get("configSrc")
+ if src:
+ src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
+ if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
+ if db.get("editingAllowedCheck"):
+ return db["editingAllowedCheck"]
+ if proj.get("editingAllowedCheck"):
+ return proj["editingAllowedCheck"]
+ return "deny"
+ except Exception:
+ return "deny"
+
+
+def assert_edit_allowed(target_path, require):
+ try:
+ rp = os.path.abspath(target_path)
+ elem_uuid = _sg_root_uuid(rp)
+ cfg_dir = None
+ bin_path = None
+ d = rp if os.path.isdir(rp) else os.path.dirname(rp)
+ for _ in range(12):
+ if not d:
+ break
+ if not elem_uuid:
+ elem_uuid = _sg_root_uuid(d + ".xml")
+ if not cfg_dir:
+ cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
+ if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
+ cfg_dir = d
+ bin_path = cand
+ if elem_uuid and cfg_dir:
+ break
+ parent = os.path.dirname(d)
+ if parent == d:
+ break
+ d = parent
+ if not elem_uuid and cfg_dir:
+ elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
+ if not bin_path or not os.path.exists(bin_path):
+ return
+ data = open(bin_path, "rb").read()
+ if len(data) <= 32:
+ return
+ if data[:3] == b"\xef\xbb\xbf":
+ data = data[3:]
+ text = data.decode("utf-8", "replace")
+ h = re.match(r"\{6,(\d+),(\d+),", text)
+ if not h:
+ return
+ g = int(h.group(1))
+ k = int(h.group(2))
+ if k == 0:
+ return
+ best = None
+ if elem_uuid:
+ for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
+ f1 = int(m.group(1))
+ if best is None or f1 < best:
+ best = f1
+ blocked = False
+ reason = ""
+ if g == 1:
+ blocked = True
+ reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
+ elif require == "removed":
+ if best is not None and best != 2:
+ blocked = True
+ reason = "объект на поддержке (не снят с поддержки) — удаление сломает обновления"
+ else:
+ if best is not None and best == 0:
+ blocked = True
+ reason = "объект на замке (поддержка поставщика) — прямая правка сломает обновления"
+ if not blocked:
+ return
+ mode = _sg_get_edit_mode(cfg_dir)
+ if mode == "off":
+ return
+ if mode == "warn":
+ sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
+ return
+ sys.stderr.write(
+ f"[support-guard] Операция запрещена: {reason}.\n"
+ f" Цель: {rp}\n"
+ f" Безопасные пути: доработка через расширение (cfe-*); включить редактирование объекта/корня в конфигураторе; снять с поддержки.\n"
+ f" Отключить проверку: editingAllowedCheck = warn|off в .v8-project.json.\n"
+ )
+ sys.exit(1)
+ except SystemExit:
+ raise
+ except Exception:
+ return
+
+
# ============================================================
# Namespaces
# ============================================================
@@ -2169,6 +2306,8 @@ def main():
resolved_path = os.path.abspath(object_path)
+ assert_edit_allowed(resolved_path, "editable")
+
# --- Load XML ---
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_tree = etree.parse(resolved_path, xml_parser)
diff --git a/.claude/skills/meta-remove/scripts/meta-remove.ps1 b/.claude/skills/meta-remove/scripts/meta-remove.ps1
index 8341cf89..c3fe406e 100644
--- a/.claude/skills/meta-remove/scripts/meta-remove.ps1
+++ b/.claude/skills/meta-remove/scripts/meta-remove.ps1
@@ -1,4 +1,4 @@
-# meta-remove v1.1 — Remove metadata object from 1C configuration dump
+# meta-remove v1.2 — Remove metadata object from 1C configuration dump
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -78,6 +78,112 @@ if (-not (Test-Path $configXml)) {
exit 1
}
+# --- Support guard (Ext/ParentConfigurations.bin) ---
+# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
+# read-only configs unless allowed. Trigger = bin present; reaction from
+# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
+# throws — guard errors degrade to allow.
+function Get-RootUuid([string]$xmlPath) {
+ if (-not (Test-Path $xmlPath)) { return $null }
+ try {
+ [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
+ $el = $mx.DocumentElement.FirstChild
+ while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
+ if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
+ } catch {}
+ return $null
+}
+function Find-V8Project([string]$startDir) {
+ $d = $startDir
+ for ($i = 0; $i -lt 20 -and $d; $i++) {
+ $pj = Join-Path $d ".v8-project.json"
+ if (Test-Path $pj) { return $pj }
+ $parent = [System.IO.Path]::GetDirectoryName($d)
+ if ($parent -eq $d) { break }
+ $d = $parent
+ }
+ return $null
+}
+function Get-EditMode([string]$cfgDir) {
+ try {
+ $pj = Find-V8Project (Get-Location).Path
+ if (-not $pj) { $pj = Find-V8Project $cfgDir }
+ if (-not $pj) { return 'deny' }
+ $proj = Get-Content -Raw $pj | ConvertFrom-Json
+ $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
+ if ($proj.databases) {
+ foreach ($db in $proj.databases) {
+ if ($db.configSrc) {
+ $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
+ if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
+ if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
+ }
+ }
+ }
+ }
+ if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
+ return 'deny'
+ } catch { return 'deny' }
+}
+function Assert-EditAllowed([string]$targetPath, [string]$require) {
+ try {
+ $rp = $targetPath
+ try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
+ $elemUuid = Get-RootUuid $rp
+ $cfgDir = $null; $binPath = $null
+ $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
+ for ($i = 0; $i -lt 12 -and $d; $i++) {
+ if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
+ if (-not $cfgDir) {
+ $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
+ if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
+ }
+ if ($elemUuid -and $cfgDir) { break }
+ $parent = [System.IO.Path]::GetDirectoryName($d)
+ if ($parent -eq $d) { break }
+ $d = $parent
+ }
+ # New object (no element file): fall back to config root uuid.
+ if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
+ if (-not $binPath -or -not (Test-Path $binPath)) { return }
+ $bytes = [System.IO.File]::ReadAllBytes($binPath)
+ if ($bytes.Length -le 32) { return }
+ $start = 0
+ if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
+ $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
+ $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
+ if (-not $hm.Success) { return }
+ $G = [int]$hm.Groups[1].Value
+ $K = [int]$hm.Groups[2].Value
+ if ($K -eq 0) { return }
+ $best = $null
+ if ($elemUuid) {
+ $u = [regex]::Escape($elemUuid.ToLower())
+ foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
+ $f1 = [int]$m.Groups[1].Value
+ if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
+ }
+ }
+ $blocked = $false; $reason = ""
+ if ($G -eq 1) { $blocked = $true; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
+ elseif ($require -eq 'removed') {
+ if ($null -ne $best -and $best -ne 2) { $blocked = $true; $reason = "объект на поддержке (не снят с поддержки) — удаление сломает обновления" }
+ }
+ else {
+ if ($null -ne $best -and $best -eq 0) { $blocked = $true; $reason = "объект на замке (поддержка поставщика) — прямая правка сломает обновления" }
+ }
+ if (-not $blocked) { return }
+ $mode = Get-EditMode $cfgDir
+ if ($mode -eq 'off') { return }
+ # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
+ # latter throws and would be swallowed by this function's own catch.
+ $msg = "[support-guard] Операция запрещена: $reason.`n Цель: $rp`n Безопасные пути: доработка через расширение (cfe-*); включить редактирование объекта/корня в конфигураторе; снять с поддержки.`n Отключить проверку: editingAllowedCheck = warn|off в .v8-project.json."
+ if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
+ [Console]::Error.WriteLine($msg)
+ exit 1
+ } catch { return }
+}
+
# --- Parse object spec ---
$parts = $Object -split "\.", 2
@@ -113,6 +219,9 @@ $typeDir = Join-Path $ConfigDir $typePlural
$objXml = Join-Path $typeDir "$objName.xml"
$objDir = Join-Path $typeDir $objName
+# Support guard — removal requires the object be снят-с-поддержки (f1=2).
+Assert-EditAllowed $objXml 'removed'
+
$hasXml = Test-Path $objXml
$hasDir = Test-Path $objDir -PathType Container
diff --git a/.claude/skills/meta-remove/scripts/meta-remove.py b/.claude/skills/meta-remove/scripts/meta-remove.py
index dc9eca1d..bd965e6d 100644
--- a/.claude/skills/meta-remove/scripts/meta-remove.py
+++ b/.claude/skills/meta-remove/scripts/meta-remove.py
@@ -1,13 +1,151 @@
#!/usr/bin/env python3
-# meta-remove v1.1 — Remove metadata object from 1C configuration dump
+# meta-remove v1.2 — Remove metadata object from 1C configuration dump
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
+import json
import os
+import re
import sys
import shutil
from lxml import etree
+
+# ============================================================
+# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
+# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
+# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
+# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
+# ============================================================
+
+def _sg_root_uuid(xml_path):
+ if not os.path.isfile(xml_path):
+ return None
+ try:
+ mx = etree.parse(xml_path).getroot()
+ for child in mx:
+ if isinstance(child.tag, str) and child.get("uuid"):
+ return child.get("uuid")
+ except Exception:
+ return None
+ return None
+
+
+def _sg_find_v8project(start_dir):
+ d = start_dir
+ for _ in range(20):
+ if not d:
+ break
+ pj = os.path.join(d, ".v8-project.json")
+ if os.path.isfile(pj):
+ return pj
+ parent = os.path.dirname(d)
+ if parent == d:
+ break
+ d = parent
+ return None
+
+
+def _sg_get_edit_mode(cfg_dir):
+ try:
+ pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
+ if not pj:
+ return "deny"
+ proj = json.loads(open(pj, encoding="utf-8-sig").read())
+ cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
+ for db in proj.get("databases", []):
+ src = db.get("configSrc")
+ if src:
+ src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
+ if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
+ if db.get("editingAllowedCheck"):
+ return db["editingAllowedCheck"]
+ if proj.get("editingAllowedCheck"):
+ return proj["editingAllowedCheck"]
+ return "deny"
+ except Exception:
+ return "deny"
+
+
+def assert_edit_allowed(target_path, require):
+ try:
+ rp = os.path.abspath(target_path)
+ elem_uuid = _sg_root_uuid(rp)
+ cfg_dir = None
+ bin_path = None
+ d = rp if os.path.isdir(rp) else os.path.dirname(rp)
+ for _ in range(12):
+ if not d:
+ break
+ if not elem_uuid:
+ elem_uuid = _sg_root_uuid(d + ".xml")
+ if not cfg_dir:
+ cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
+ if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
+ cfg_dir = d
+ bin_path = cand
+ if elem_uuid and cfg_dir:
+ break
+ parent = os.path.dirname(d)
+ if parent == d:
+ break
+ d = parent
+ if not elem_uuid and cfg_dir:
+ elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
+ if not bin_path or not os.path.exists(bin_path):
+ return
+ data = open(bin_path, "rb").read()
+ if len(data) <= 32:
+ return
+ if data[:3] == b"\xef\xbb\xbf":
+ data = data[3:]
+ text = data.decode("utf-8", "replace")
+ h = re.match(r"\{6,(\d+),(\d+),", text)
+ if not h:
+ return
+ g = int(h.group(1))
+ k = int(h.group(2))
+ if k == 0:
+ return
+ best = None
+ if elem_uuid:
+ for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
+ f1 = int(m.group(1))
+ if best is None or f1 < best:
+ best = f1
+ blocked = False
+ reason = ""
+ if g == 1:
+ blocked = True
+ reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
+ elif require == "removed":
+ if best is not None and best != 2:
+ blocked = True
+ reason = "объект на поддержке (не снят с поддержки) — удаление сломает обновления"
+ else:
+ if best is not None and best == 0:
+ blocked = True
+ reason = "объект на замке (поддержка поставщика) — прямая правка сломает обновления"
+ if not blocked:
+ return
+ mode = _sg_get_edit_mode(cfg_dir)
+ if mode == "off":
+ return
+ if mode == "warn":
+ sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
+ return
+ sys.stderr.write(
+ f"[support-guard] Операция запрещена: {reason}.\n"
+ f" Цель: {rp}\n"
+ f" Безопасные пути: доработка через расширение (cfe-*); включить редактирование объекта/корня в конфигураторе; снять с поддержки.\n"
+ f" Отключить проверку: editingAllowedCheck = warn|off в .v8-project.json.\n"
+ )
+ sys.exit(1)
+ except SystemExit:
+ raise
+ except Exception:
+ return
+
# --- Type -> plural directory mapping ---
TYPE_PLURAL_MAP = {
@@ -161,6 +299,9 @@ def main():
obj_xml = os.path.join(type_dir, f"{obj_name}.xml")
obj_dir = os.path.join(type_dir, obj_name)
+ # Support guard — removal requires the object be снят-с-поддержки (f1=2).
+ assert_edit_allowed(obj_xml, "removed")
+
has_xml = os.path.isfile(obj_xml)
has_dir = os.path.isdir(obj_dir)
diff --git a/docs/v8-project-guide.md b/docs/v8-project-guide.md
index 7fd7e70b..f8bfe8ab 100644
--- a/docs/v8-project-guide.md
+++ b/docs/v8-project-guide.md
@@ -57,6 +57,7 @@
| `v8path` | string | да | — | Путь к каталогу `bin` платформы 1С | `/db-list add` или руками |
| `databases` | array | да | — | Список баз данных | `/db-list add` |
| `default` | string | нет | — | `id` базы по умолчанию | `/db-list` |
+| `editingAllowedCheck` | `"deny"`/`"warn"`/`"off"` | нет | `deny` | Глобальная реакция support-guard на правку объектов на замке (см. ниже) | Руками |
| `webPath` | string | нет | `tools/apache24` | Каталог Apache HTTP Server | Руками |
| `ffmpegPath` | string | нет | `tools/ffmpeg/bin/ffmpeg.exe` | Путь к ffmpeg | Руками |
| `tts` | object | нет | Edge TTS, DmitryNeural | Настройки озвучки видео | Руками |
@@ -76,8 +77,20 @@
| `aliases` | string[] | нет | Альтернативные имена для обращения к базе | `/db-list add` или руками |
| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`) | Руками |
| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации | Руками |
+| `editingAllowedCheck` | `"deny"`/`"warn"`/`"off"` | нет | Override реакции support-guard для этой базы (см. ниже) | Руками |
| `webUrl` | string | нет | URL веб-клиента для `/web-test` | Руками |
+### Support-guard и `editingAllowedCheck`
+
+Навыки-мутаторы (`meta-edit`, `meta-compile`, `meta-remove` и др.) перед изменением исходников проверяют состояние поддержки конфигурации (`Ext/ParentConfigurations.bin`, см. [1c-support-state-spec.md](1c-support-state-spec.md)). Если объект «на замке» поставщика (или вся конфигурация read-only, или удаляется не снятый с поддержки объект), правка по умолчанию **блокируется** — прямое изменение сломало бы обновления.
+
+Реакцию задаёт `editingAllowedCheck`:
+- `deny` (по умолчанию, в т.ч. когда поле не задано) — блокировать с диагностикой;
+- `warn` — пропускать, но писать предупреждение;
+- `off` — проверку не выполнять.
+
+Триггер проверки — наличие `ParentConfigurations.bin` (конфигурация на поддержке), а не регистрация в `.v8-project.json`. Поле лишь меняет реакцию. Берётся `databases[].editingAllowedCheck` базы, чей `configSrc` охватывает редактируемый путь; иначе — корневое `editingAllowedCheck`; иначе `deny`.
+
### Разрешение базы
Все навыки `/db-*`, `/epf-build`, `/epf-dump`, `/erf-build`, `/erf-dump`, `/web-publish` используют единый алгоритм:
diff --git a/tests/skills/cases/meta-compile/fixtures/on-support/Catalogs/Locked.xml b/tests/skills/cases/meta-compile/fixtures/on-support/Catalogs/Locked.xml
new file mode 100644
index 00000000..669bb616
--- /dev/null
+++ b/tests/skills/cases/meta-compile/fixtures/on-support/Catalogs/Locked.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ Locked
+
+ 9
+ 25
+
+
+
+
diff --git a/tests/skills/cases/meta-compile/fixtures/on-support/Catalogs/Removed.xml b/tests/skills/cases/meta-compile/fixtures/on-support/Catalogs/Removed.xml
new file mode 100644
index 00000000..7f0e0e2f
--- /dev/null
+++ b/tests/skills/cases/meta-compile/fixtures/on-support/Catalogs/Removed.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ Removed
+
+ 9
+ 25
+
+
+
+
diff --git a/tests/skills/cases/meta-compile/fixtures/on-support/Configuration.xml b/tests/skills/cases/meta-compile/fixtures/on-support/Configuration.xml
new file mode 100644
index 00000000..c336d3aa
--- /dev/null
+++ b/tests/skills/cases/meta-compile/fixtures/on-support/Configuration.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ ТестКонфиг
+ ТестВендор
+ 1.0
+
+
+ Locked
+ Removed
+
+
+
diff --git a/tests/skills/cases/meta-compile/fixtures/on-support/Ext/ParentConfigurations.bin b/tests/skills/cases/meta-compile/fixtures/on-support/Ext/ParentConfigurations.bin
new file mode 100644
index 00000000..e96a02d0
--- /dev/null
+++ b/tests/skills/cases/meta-compile/fixtures/on-support/Ext/ParentConfigurations.bin
@@ -0,0 +1 @@
+{6,0,1,aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa,0,bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb,"1.0","ТестВендор","ТестКонфиг",3,0,0,11111111-1111-1111-1111-111111111111,0,0,22222222-2222-2222-2222-222222222222,22222222-2222-2222-2222-222222222222,2,0,33333333-3333-3333-3333-333333333333,33333333-3333-3333-3333-333333333333}
\ No newline at end of file
diff --git a/tests/skills/cases/meta-compile/guard-deny-root.json b/tests/skills/cases/meta-compile/guard-deny-root.json
new file mode 100644
index 00000000..89abc80a
--- /dev/null
+++ b/tests/skills/cases/meta-compile/guard-deny-root.json
@@ -0,0 +1,6 @@
+{
+ "name": "Guard: добавление объекта при закрытом корне (f1=0) запрещено",
+ "setup": "fixture:on-support",
+ "input": { "type": "Catalog", "name": "НовыйСправочник" },
+ "expectError": "support-guard"
+}
diff --git a/tests/skills/cases/meta-edit/fixtures/on-support/Catalogs/Locked.xml b/tests/skills/cases/meta-edit/fixtures/on-support/Catalogs/Locked.xml
new file mode 100644
index 00000000..669bb616
--- /dev/null
+++ b/tests/skills/cases/meta-edit/fixtures/on-support/Catalogs/Locked.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ Locked
+
+ 9
+ 25
+
+
+
+
diff --git a/tests/skills/cases/meta-edit/fixtures/on-support/Catalogs/Removed.xml b/tests/skills/cases/meta-edit/fixtures/on-support/Catalogs/Removed.xml
new file mode 100644
index 00000000..7f0e0e2f
--- /dev/null
+++ b/tests/skills/cases/meta-edit/fixtures/on-support/Catalogs/Removed.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ Removed
+
+ 9
+ 25
+
+
+
+
diff --git a/tests/skills/cases/meta-edit/fixtures/on-support/Configuration.xml b/tests/skills/cases/meta-edit/fixtures/on-support/Configuration.xml
new file mode 100644
index 00000000..c336d3aa
--- /dev/null
+++ b/tests/skills/cases/meta-edit/fixtures/on-support/Configuration.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ ТестКонфиг
+ ТестВендор
+ 1.0
+
+
+ Locked
+ Removed
+
+
+
diff --git a/tests/skills/cases/meta-edit/fixtures/on-support/Ext/ParentConfigurations.bin b/tests/skills/cases/meta-edit/fixtures/on-support/Ext/ParentConfigurations.bin
new file mode 100644
index 00000000..e96a02d0
--- /dev/null
+++ b/tests/skills/cases/meta-edit/fixtures/on-support/Ext/ParentConfigurations.bin
@@ -0,0 +1 @@
+{6,0,1,aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa,0,bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb,"1.0","ТестВендор","ТестКонфиг",3,0,0,11111111-1111-1111-1111-111111111111,0,0,22222222-2222-2222-2222-222222222222,22222222-2222-2222-2222-222222222222,2,0,33333333-3333-3333-3333-333333333333,33333333-3333-3333-3333-333333333333}
\ No newline at end of file
diff --git a/tests/skills/cases/meta-edit/guard-deny-locked.json b/tests/skills/cases/meta-edit/guard-deny-locked.json
new file mode 100644
index 00000000..6bf3b202
--- /dev/null
+++ b/tests/skills/cases/meta-edit/guard-deny-locked.json
@@ -0,0 +1,7 @@
+{
+ "name": "Guard: правка объекта на замке (f1=0) запрещена",
+ "setup": "fixture:on-support",
+ "params": { "objectPath": "Catalogs/Locked.xml" },
+ "input": { "operations": [ { "op": "add-attribute", "name": "ТестРеквизит", "type": "String", "length": 10 } ] },
+ "expectError": "support-guard"
+}
diff --git a/tests/skills/cases/meta-remove/fixtures/on-support/Catalogs/Locked.xml b/tests/skills/cases/meta-remove/fixtures/on-support/Catalogs/Locked.xml
new file mode 100644
index 00000000..669bb616
--- /dev/null
+++ b/tests/skills/cases/meta-remove/fixtures/on-support/Catalogs/Locked.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ Locked
+
+ 9
+ 25
+
+
+
+
diff --git a/tests/skills/cases/meta-remove/fixtures/on-support/Catalogs/Removed.xml b/tests/skills/cases/meta-remove/fixtures/on-support/Catalogs/Removed.xml
new file mode 100644
index 00000000..7f0e0e2f
--- /dev/null
+++ b/tests/skills/cases/meta-remove/fixtures/on-support/Catalogs/Removed.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ Removed
+
+ 9
+ 25
+
+
+
+
diff --git a/tests/skills/cases/meta-remove/fixtures/on-support/Configuration.xml b/tests/skills/cases/meta-remove/fixtures/on-support/Configuration.xml
new file mode 100644
index 00000000..c336d3aa
--- /dev/null
+++ b/tests/skills/cases/meta-remove/fixtures/on-support/Configuration.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ ТестКонфиг
+ ТестВендор
+ 1.0
+
+
+ Locked
+ Removed
+
+
+
diff --git a/tests/skills/cases/meta-remove/fixtures/on-support/Ext/ParentConfigurations.bin b/tests/skills/cases/meta-remove/fixtures/on-support/Ext/ParentConfigurations.bin
new file mode 100644
index 00000000..e96a02d0
--- /dev/null
+++ b/tests/skills/cases/meta-remove/fixtures/on-support/Ext/ParentConfigurations.bin
@@ -0,0 +1 @@
+{6,0,1,aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa,0,bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb,"1.0","ТестВендор","ТестКонфиг",3,0,0,11111111-1111-1111-1111-111111111111,0,0,22222222-2222-2222-2222-222222222222,22222222-2222-2222-2222-222222222222,2,0,33333333-3333-3333-3333-333333333333,33333333-3333-3333-3333-333333333333}
\ No newline at end of file
diff --git a/tests/skills/cases/meta-remove/guard-deny-locked.json b/tests/skills/cases/meta-remove/guard-deny-locked.json
new file mode 100644
index 00000000..99577d2a
--- /dev/null
+++ b/tests/skills/cases/meta-remove/guard-deny-locked.json
@@ -0,0 +1,6 @@
+{
+ "name": "Guard: удаление объекта на замке (f1=0) запрещено",
+ "setup": "fixture:on-support",
+ "object": "Catalog.Locked",
+ "expectError": "support-guard"
+}