From 2136245b6996d4acc390d57cdb768ffd73e80fca Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 20 Jun 2026 14:57:14 +0300 Subject: [PATCH] =?UTF-8?q?feat(meta-edit,meta-compile,meta-remove):=20sup?= =?UTF-8?q?port-guard=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BA=D0=BE=D0=B9=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Пилот энфорсмента issue #23: перед записью навыки-мутаторы проверяют состояние поддержки (Ext/ParentConfigurations.bin) и блокируют опасную правочку. Триггер — наличие bin (конфиг на поддержке); реакция из .v8-project.json editingAllowedCheck (deny|warn|off, по умолчанию deny). Assert-EditAllowed (нативная копия в каждом навыке, оба порта): walk-up резолвит uuid цели (объект / владелец / корень — по пути) и корень конфигурации, затем G-vs-f1 и консервативная свёртка min(f1). Два режима: require-editable (f1≥1, G≠1) для правок/добавлений; require-removed (f1=2) для удаления. - meta-edit (v1.7): editable на редактируемом объекте; - meta-compile (v1.13): editable на корне (добавление нового объекта); - meta-remove (v1.2): removed на удаляемом объекте. Диагностика через [Console]::Error.WriteLine + exit 1 (не Write-Error: под ErrorActionPreference=Stop тот бросает и был бы проглочен catch'ем). Тесты: малая on-support фикстура с рукотворным bin (root/Locked f1=0, Removed f1=2); guard-deny кейсы (expectError) — оба рантайма зелёные, старые кейсы не сломаны (конфиги без bin → allow). Поле editingAllowedCheck задокументировано в docs/v8-project-guide.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../meta-compile/scripts/meta-compile.ps1 | 110 +++++++++++++- .../meta-compile/scripts/meta-compile.py | 140 ++++++++++++++++- .../skills/meta-edit/scripts/meta-edit.ps1 | 110 +++++++++++++- .claude/skills/meta-edit/scripts/meta-edit.py | 141 ++++++++++++++++- .../meta-remove/scripts/meta-remove.ps1 | 111 +++++++++++++- .../skills/meta-remove/scripts/meta-remove.py | 143 +++++++++++++++++- docs/v8-project-guide.md | 13 ++ .../fixtures/on-support/Catalogs/Locked.xml | 12 ++ .../fixtures/on-support/Catalogs/Removed.xml | 12 ++ .../fixtures/on-support/Configuration.xml | 14 ++ .../on-support/Ext/ParentConfigurations.bin | 1 + .../cases/meta-compile/guard-deny-root.json | 6 + .../fixtures/on-support/Catalogs/Locked.xml | 12 ++ .../fixtures/on-support/Catalogs/Removed.xml | 12 ++ .../fixtures/on-support/Configuration.xml | 14 ++ .../on-support/Ext/ParentConfigurations.bin | 1 + .../cases/meta-edit/guard-deny-locked.json | 7 + .../fixtures/on-support/Catalogs/Locked.xml | 12 ++ .../fixtures/on-support/Catalogs/Removed.xml | 12 ++ .../fixtures/on-support/Configuration.xml | 14 ++ .../on-support/Ext/ParentConfigurations.bin | 1 + .../cases/meta-remove/guard-deny-locked.json | 6 + 22 files changed, 898 insertions(+), 6 deletions(-) create mode 100644 tests/skills/cases/meta-compile/fixtures/on-support/Catalogs/Locked.xml create mode 100644 tests/skills/cases/meta-compile/fixtures/on-support/Catalogs/Removed.xml create mode 100644 tests/skills/cases/meta-compile/fixtures/on-support/Configuration.xml create mode 100644 tests/skills/cases/meta-compile/fixtures/on-support/Ext/ParentConfigurations.bin create mode 100644 tests/skills/cases/meta-compile/guard-deny-root.json create mode 100644 tests/skills/cases/meta-edit/fixtures/on-support/Catalogs/Locked.xml create mode 100644 tests/skills/cases/meta-edit/fixtures/on-support/Catalogs/Removed.xml create mode 100644 tests/skills/cases/meta-edit/fixtures/on-support/Configuration.xml create mode 100644 tests/skills/cases/meta-edit/fixtures/on-support/Ext/ParentConfigurations.bin create mode 100644 tests/skills/cases/meta-edit/guard-deny-locked.json create mode 100644 tests/skills/cases/meta-remove/fixtures/on-support/Catalogs/Locked.xml create mode 100644 tests/skills/cases/meta-remove/fixtures/on-support/Catalogs/Removed.xml create mode 100644 tests/skills/cases/meta-remove/fixtures/on-support/Configuration.xml create mode 100644 tests/skills/cases/meta-remove/fixtures/on-support/Ext/ParentConfigurations.bin create mode 100644 tests/skills/cases/meta-remove/guard-deny-locked.json 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" +}