feat(mutators): размножить support-guard на остальные навыки-мутаторы

Контракт Assert-EditAllowed (дословная копия из meta-edit, оба порта)
добавлен в 13 навыков-мутаторов. Всем единый вызов require=editable —
«кого проверять» решает не навык, а walk-up по файловой системе от пути
цели:
  - целевой файл имеет root-uuid → его f1 (правка существующего элемента);
  - иначе ближайший вверх <dir>.xml с uuid → f1 владельца (добавление
    дочернего: форма/реквизит/макет к объекту);
  - иначе корень Configuration.xml → f1 корня (новый объект верхнего уровня).

Это воспроизводит семантику поддержки 1С автоматически, поэтому один и
тот же навык проверяет разное по состоянию дампа: skd-compile/mxl-compile/
form-compile в существующий макет/форму → f1 этого элемента (modify), в
несуществующий → f1 владельца. Проверено обоими сценариями.

Навыки: form-edit, form-add, form-compile, skd-edit, skd-compile, cf-edit,
subsystem-edit, subsystem-compile, interface-edit, template-add, help-add,
mxl-compile, role-compile. Диагностика через [Console]::Error.WriteLine +
exit 1 (как в эталоне). Версии подняты в обоих портах.

Проверено: deny на обоих портах (крафт-фикстуры на копиях, корпус не
тронут); все 16 навыков-мутаторов зелёные на PowerShell и Python
(264 кейса). BOM сохранён везде.

Follow-up (в upload-плане): пред-существующий баг help-add
Detect-FormatVersion (Substring на кириллице, до гарда, не связан);
авторезолв пути в meta-edit; per-skill committed deny-тесты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-06-20 15:22:14 +03:00
parent 2136245b69
commit b17dd5d04c
26 changed files with 3236 additions and 26 deletions
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# interface-edit v1.3 — Edit 1C CommandInterface.xml
# interface-edit v1.4 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -10,6 +10,143 @@ import subprocess
import sys
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
def detect_format_version(d):
while d:
cfg_path = os.path.join(d, "Configuration.xml")
@@ -208,6 +345,8 @@ def main():
ci_path = os.path.join(os.getcwd(), ci_path)
resolved_path = ci_path
assert_edit_allowed(ci_path, "editable")
# --- Create if missing ---
if not os.path.isfile(ci_path):
if args.CreateIfMissing: