diff --git a/.claude/skills/cf-edit/scripts/cf-edit.ps1 b/.claude/skills/cf-edit/scripts/cf-edit.ps1
index e9a3364f..27dd3b4c 100644
--- a/.claude/skills/cf-edit/scripts/cf-edit.ps1
+++ b/.claude/skills/cf-edit/scripts/cf-edit.ps1
@@ -1,4 +1,4 @@
-# cf-edit v1.17 — Edit 1C configuration root (Configuration.xml)
+# cf-edit v1.18 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
diff --git a/.claude/skills/cf-edit/scripts/cf-edit.py b/.claude/skills/cf-edit/scripts/cf-edit.py
index 2c127e30..94454fce 100644
--- a/.claude/skills/cf-edit/scripts/cf-edit.py
+++ b/.claude/skills/cf-edit/scripts/cf-edit.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# cf-edit v1.17 — Edit 1C configuration root (Configuration.xml)
+# cf-edit v1.18 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -14,6 +14,43 @@ from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
+class CIDict(dict):
+ # Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
+ # реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
+ # сохраняется — от него зависит порядок эмиссии.
+ def _actual(self, key):
+ if not isinstance(key, str) or dict.__contains__(self, key):
+ return key
+ ci = self.__dict__.get('_ci')
+ if ci is None or len(ci) != len(self):
+ ci = {k.lower(): k for k in self if isinstance(k, str)}
+ self.__dict__['_ci'] = ci
+ return ci.get(key.lower(), key)
+
+ def __getitem__(self, key):
+ return dict.__getitem__(self, self._actual(key))
+
+ def __contains__(self, key):
+ return dict.__contains__(self, self._actual(key))
+
+ def get(self, key, default=None):
+ return dict.get(self, self._actual(key), default)
+
+ def pop(self, key, *default):
+ return dict.pop(self, self._actual(key), *default)
+
+ def __setitem__(self, key, value):
+ # запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
+ dict.__setitem__(self, self._actual(key), value)
+
+def ci_json(obj):
+ """Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
+ if isinstance(obj, dict):
+ return CIDict((k, ci_json(v)) for k, v in obj.items())
+ if isinstance(obj, list):
+ return [ci_json(v) for v in obj]
+ return obj
+
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
@@ -785,7 +822,7 @@ def main():
layout = value
if isinstance(layout, str):
try:
- layout = json.loads(layout)
+ layout = ci_json(json.loads(layout))
except json.JSONDecodeError:
print(f"set-panels value must be valid JSON object", file=sys.stderr)
sys.exit(1)
@@ -940,7 +977,7 @@ def main():
layout = value
if isinstance(layout, str):
try:
- layout = json.loads(layout)
+ layout = ci_json(json.loads(layout))
except json.JSONDecodeError:
print("set-home-page value must be valid JSON object", file=sys.stderr)
sys.exit(1)
@@ -1008,7 +1045,7 @@ def main():
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh:
- ops = json.loads(fh.read())
+ ops = ci_json(json.loads(fh.read()))
if isinstance(ops, list):
operations = ops
else:
diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1
index 7e772354..014e9f8e 100644
--- a/.claude/skills/form-compile/scripts/form-compile.ps1
+++ b/.claude/skills/form-compile/scripts/form-compile.ps1
@@ -1,4 +1,4 @@
-# form-compile v1.190 — Compile 1C managed form from JSON or object metadata (+write_xml_file/write_utf8_bom: общий эталон записи)
+# form-compile v1.191 — Compile 1C managed form from JSON or object metadata (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$JsonPath,
diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py
index 78218b29..b6cfde44 100644
--- a/.claude/skills/form-compile/scripts/form-compile.py
+++ b/.claude/skills/form-compile/scripts/form-compile.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# form-compile v1.190 — Compile 1C managed form from JSON or object metadata (+write_xml_file/write_utf8_bom: общий эталон записи)
+# form-compile v1.191 — Compile 1C managed form from JSON or object metadata (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -15,6 +15,43 @@ from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
+class CIDict(dict):
+ # Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
+ # реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
+ # сохраняется — от него зависит порядок эмиссии.
+ def _actual(self, key):
+ if not isinstance(key, str) or dict.__contains__(self, key):
+ return key
+ ci = self.__dict__.get('_ci')
+ if ci is None or len(ci) != len(self):
+ ci = {k.lower(): k for k in self if isinstance(k, str)}
+ self.__dict__['_ci'] = ci
+ return ci.get(key.lower(), key)
+
+ def __getitem__(self, key):
+ return dict.__getitem__(self, self._actual(key))
+
+ def __contains__(self, key):
+ return dict.__contains__(self, self._actual(key))
+
+ def get(self, key, default=None):
+ return dict.get(self, self._actual(key), default)
+
+ def pop(self, key, *default):
+ return dict.pop(self, self._actual(key), *default)
+
+ def __setitem__(self, key, value):
+ # запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
+ dict.__setitem__(self, self._actual(key), value)
+
+def ci_json(obj):
+ """Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
+ if isinstance(obj, dict):
+ return CIDict((k, ci_json(v)) for k, v in obj.items())
+ if isinstance(obj, list):
+ return [ci_json(v) for v in obj]
+ return obj
+
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
@@ -506,7 +543,7 @@ def load_preset(preset_name, script_dir, out_path_resolved):
built_in_path = os.path.join(preset_dir, f'{preset_name}.json')
if os.path.isfile(built_in_path):
with open(built_in_path, 'r', encoding='utf-8-sig') as f:
- preset_data = json.load(f)
+ preset_data = ci_json(json.load(f))
for k in list(preset_data.keys()):
defaults[k] = _deep_merge(defaults.get(k), preset_data[k])
@@ -6399,7 +6436,7 @@ def main():
json.dump(dsl, f, ensure_ascii=False, indent=2)
print(f"[from-object] DSL saved: {dsl_path}")
- defn = json.loads(json.dumps(dsl)) # normalize OrderedDict to regular dict
+ defn = ci_json(json.loads(json.dumps(dsl))) # normalize OrderedDict to regular dict
# Convert dict-style elements (from generators) to list-style (expected by compiler)
defn = _normalize_elements(defn)
else:
@@ -6410,7 +6447,7 @@ def main():
sys.exit(1)
with open(json_path, 'r', encoding='utf-8-sig') as f:
- defn = json.load(f)
+ defn = ci_json(json.load(f))
global QUERY_BASE_DIR
QUERY_BASE_DIR = os.path.dirname(os.path.abspath(json_path))
diff --git a/.claude/skills/form-edit/scripts/form-edit.ps1 b/.claude/skills/form-edit/scripts/form-edit.ps1
index 5284c703..9f30e6e2 100644
--- a/.claude/skills/form-edit/scripts/form-edit.ps1
+++ b/.claude/skills/form-edit/scripts/form-edit.ps1
@@ -1,4 +1,4 @@
-# form-edit v1.13 — Edit 1C managed form elements (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
+# form-edit v1.14 — Edit 1C managed form elements (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
diff --git a/.claude/skills/form-edit/scripts/form-edit.py b/.claude/skills/form-edit/scripts/form-edit.py
index cdd47d6e..b8cf7010 100644
--- a/.claude/skills/form-edit/scripts/form-edit.py
+++ b/.claude/skills/form-edit/scripts/form-edit.py
@@ -1,4 +1,4 @@
-# form-edit v1.13 — Edit 1C managed form elements (Python port) (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
+# form-edit v1.14 — Edit 1C managed form elements (Python port) (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -13,6 +13,43 @@ sys.stderr.reconfigure(encoding="utf-8")
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
+class CIDict(dict):
+ # Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
+ # реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
+ # сохраняется — от него зависит порядок эмиссии.
+ def _actual(self, key):
+ if not isinstance(key, str) or dict.__contains__(self, key):
+ return key
+ ci = self.__dict__.get('_ci')
+ if ci is None or len(ci) != len(self):
+ ci = {k.lower(): k for k in self if isinstance(k, str)}
+ self.__dict__['_ci'] = ci
+ return ci.get(key.lower(), key)
+
+ def __getitem__(self, key):
+ return dict.__getitem__(self, self._actual(key))
+
+ def __contains__(self, key):
+ return dict.__contains__(self, self._actual(key))
+
+ def get(self, key, default=None):
+ return dict.get(self, self._actual(key), default)
+
+ def pop(self, key, *default):
+ return dict.pop(self, self._actual(key), *default)
+
+ def __setitem__(self, key, value):
+ # запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
+ dict.__setitem__(self, self._actual(key), value)
+
+def ci_json(obj):
+ """Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
+ if isinstance(obj, dict):
+ return CIDict((k, ci_json(v)) for k, v in obj.items())
+ if isinstance(obj, list):
+ return [ci_json(v) for v in obj]
+ return obj
+
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
@@ -281,7 +318,7 @@ root = tree.getroot()
# ── 2. Load JSON ────────────────────────────────────────────
with open(json_path, "r", encoding="utf-8-sig") as f:
- defn = json.load(f)
+ defn = ci_json(json.load(f))
# ── 3. Form name + header ───────────────────────────────────
diff --git a/.claude/skills/interface-edit/scripts/interface-edit.ps1 b/.claude/skills/interface-edit/scripts/interface-edit.ps1
index 79a6c85b..f875bc29 100644
--- a/.claude/skills/interface-edit/scripts/interface-edit.ps1
+++ b/.claude/skills/interface-edit/scripts/interface-edit.ps1
@@ -1,4 +1,4 @@
-# interface-edit v1.16 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
+# interface-edit v1.17 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
diff --git a/.claude/skills/interface-edit/scripts/interface-edit.py b/.claude/skills/interface-edit/scripts/interface-edit.py
index 2057e81e..c462cdd9 100644
--- a/.claude/skills/interface-edit/scripts/interface-edit.py
+++ b/.claude/skills/interface-edit/scripts/interface-edit.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# interface-edit v1.16 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
+# interface-edit v1.17 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -12,6 +12,43 @@ from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
+class CIDict(dict):
+ # Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
+ # реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
+ # сохраняется — от него зависит порядок эмиссии.
+ def _actual(self, key):
+ if not isinstance(key, str) or dict.__contains__(self, key):
+ return key
+ ci = self.__dict__.get('_ci')
+ if ci is None or len(ci) != len(self):
+ ci = {k.lower(): k for k in self if isinstance(k, str)}
+ self.__dict__['_ci'] = ci
+ return ci.get(key.lower(), key)
+
+ def __getitem__(self, key):
+ return dict.__getitem__(self, self._actual(key))
+
+ def __contains__(self, key):
+ return dict.__contains__(self, self._actual(key))
+
+ def get(self, key, default=None):
+ return dict.get(self, self._actual(key), default)
+
+ def pop(self, key, *default):
+ return dict.pop(self, self._actual(key), *default)
+
+ def __setitem__(self, key, value):
+ # запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
+ dict.__setitem__(self, self._actual(key), value)
+
+def ci_json(obj):
+ """Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
+ if isinstance(obj, dict):
+ return CIDict((k, ci_json(v)) for k, v in obj.items())
+ if isinstance(obj, list):
+ return [ci_json(v) for v in obj]
+ return obj
+
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
@@ -314,7 +351,7 @@ def import_ci_fragment(xml_string):
def parse_value_list(val):
val = val.strip()
if val.startswith("["):
- arr = json.loads(val)
+ arr = ci_json(json.loads(val))
return [str(item) for item in arr]
return [val]
@@ -610,7 +647,7 @@ def main():
def do_place(json_val):
nonlocal add_count, modify_count
- defn = json_val if isinstance(json_val, dict) else json.loads(json_val)
+ defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
cmd_name = normalize_cmd_name(str(defn["command"]))
group_name = str(defn["group"])
if not cmd_name or not group_name:
@@ -638,7 +675,7 @@ def main():
def do_order(json_val):
nonlocal add_count, remove_count
- defn = json_val if isinstance(json_val, dict) else json.loads(json_val)
+ defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
group_name = str(defn["group"])
commands = [normalize_cmd_name(str(c)) for c in defn["commands"]]
if not group_name or not commands:
@@ -672,7 +709,7 @@ def main():
def do_subsystem_order(json_val):
nonlocal add_count, remove_count
- parsed = json_val if isinstance(json_val, list) else json.loads(json_val)
+ parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
subsystems = [str(s) for s in parsed]
if not subsystems:
print("subsystem-order requires array of subsystem paths", file=sys.stderr)
@@ -697,7 +734,7 @@ def main():
def do_group_order(json_val):
nonlocal add_count, remove_count
- parsed = json_val if isinstance(json_val, list) else json.loads(json_val)
+ parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
groups = [str(g) for g in parsed]
if not groups:
print("group-order requires array of group names", file=sys.stderr)
@@ -727,7 +764,7 @@ def main():
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh:
- ops = json.loads(fh.read())
+ ops = ci_json(json.loads(fh.read()))
if isinstance(ops, list):
operations = ops
else:
diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1
index e919b164..36c76a79 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.36 — Edit existing 1C metadata object XML (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
+# meta-edit v1.37 — Edit existing 1C metadata object XML (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
diff --git a/.claude/skills/meta-edit/scripts/meta-edit.py b/.claude/skills/meta-edit/scripts/meta-edit.py
index 7776d0b6..6822da34 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.36 — Edit existing 1C metadata object XML (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
+# meta-edit v1.37 — Edit existing 1C metadata object XML (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -13,6 +13,43 @@ from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
+class CIDict(dict):
+ # Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
+ # реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
+ # сохраняется — от него зависит порядок эмиссии.
+ def _actual(self, key):
+ if not isinstance(key, str) or dict.__contains__(self, key):
+ return key
+ ci = self.__dict__.get('_ci')
+ if ci is None or len(ci) != len(self):
+ ci = {k.lower(): k for k in self if isinstance(k, str)}
+ self.__dict__['_ci'] = ci
+ return ci.get(key.lower(), key)
+
+ def __getitem__(self, key):
+ return dict.__getitem__(self, self._actual(key))
+
+ def __contains__(self, key):
+ return dict.__contains__(self, self._actual(key))
+
+ def get(self, key, default=None):
+ return dict.get(self, self._actual(key), default)
+
+ def pop(self, key, *default):
+ return dict.pop(self, self._actual(key), *default)
+
+ def __setitem__(self, key, value):
+ # запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
+ dict.__setitem__(self, self._actual(key), value)
+
+def ci_json(obj):
+ """Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
+ if isinstance(obj, dict):
+ return CIDict((k, ci_json(v)) for k, v in obj.items())
+ if isinstance(obj, list):
+ return [ci_json(v) for v in obj]
+ return obj
+
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
@@ -3210,7 +3247,7 @@ def main():
if not os.path.exists(args.DefinitionFile):
die(f"Definition file not found: {args.DefinitionFile}")
with open(args.DefinitionFile, "r", encoding="utf-8-sig") as f:
- definition = json.load(f)
+ definition = ci_json(json.load(f))
# --- Resolve object path ---
object_path = args.ObjectPath
diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1
index 950c5b07..ffb6ffb9 100644
--- a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1
+++ b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1
@@ -1,4 +1,4 @@
-# mxl-compile v1.13 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
+# mxl-compile v1.14 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py
index 15b0ab44..68685cf2 100644
--- a/.claude/skills/mxl-compile/scripts/mxl-compile.py
+++ b/.claude/skills/mxl-compile/scripts/mxl-compile.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# mxl-compile v1.13 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
+# mxl-compile v1.14 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -12,6 +12,43 @@ from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
+class CIDict(dict):
+ # Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
+ # реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
+ # сохраняется — от него зависит порядок эмиссии.
+ def _actual(self, key):
+ if not isinstance(key, str) or dict.__contains__(self, key):
+ return key
+ ci = self.__dict__.get('_ci')
+ if ci is None or len(ci) != len(self):
+ ci = {k.lower(): k for k in self if isinstance(k, str)}
+ self.__dict__['_ci'] = ci
+ return ci.get(key.lower(), key)
+
+ def __getitem__(self, key):
+ return dict.__getitem__(self, self._actual(key))
+
+ def __contains__(self, key):
+ return dict.__contains__(self, self._actual(key))
+
+ def get(self, key, default=None):
+ return dict.get(self, self._actual(key), default)
+
+ def pop(self, key, *default):
+ return dict.pop(self, self._actual(key), *default)
+
+ def __setitem__(self, key, value):
+ # запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
+ dict.__setitem__(self, self._actual(key), value)
+
+def ci_json(obj):
+ """Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
+ if isinstance(obj, dict):
+ return CIDict((k, ci_json(v)) for k, v in obj.items())
+ if isinstance(obj, list):
+ return [ci_json(v) for v in obj]
+ return obj
+
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
@@ -281,7 +318,7 @@ def main():
sys.exit(1)
with open(json_path, 'r', encoding='utf-8-sig') as f:
- defn = json.load(f)
+ defn = ci_json(json.load(f))
if not defn.get('columns'):
print("Required field 'columns' is missing", file=sys.stderr)
diff --git a/.claude/skills/role-compile/scripts/role-compile.ps1 b/.claude/skills/role-compile/scripts/role-compile.ps1
index 2e0df6ca..189853ac 100644
--- a/.claude/skills/role-compile/scripts/role-compile.ps1
+++ b/.claude/skills/role-compile/scripts/role-compile.ps1
@@ -1,4 +1,4 @@
-# role-compile v1.23 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без)
+# role-compile v1.24 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
diff --git a/.claude/skills/role-compile/scripts/role-compile.py b/.claude/skills/role-compile/scripts/role-compile.py
index 7f7e56e2..c769e54b 100644
--- a/.claude/skills/role-compile/scripts/role-compile.py
+++ b/.claude/skills/role-compile/scripts/role-compile.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# role-compile v1.23 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без)
+# role-compile v1.24 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -12,6 +12,43 @@ from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
+class CIDict(dict):
+ # Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
+ # реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
+ # сохраняется — от него зависит порядок эмиссии.
+ def _actual(self, key):
+ if not isinstance(key, str) or dict.__contains__(self, key):
+ return key
+ ci = self.__dict__.get('_ci')
+ if ci is None or len(ci) != len(self):
+ ci = {k.lower(): k for k in self if isinstance(k, str)}
+ self.__dict__['_ci'] = ci
+ return ci.get(key.lower(), key)
+
+ def __getitem__(self, key):
+ return dict.__getitem__(self, self._actual(key))
+
+ def __contains__(self, key):
+ return dict.__contains__(self, self._actual(key))
+
+ def get(self, key, default=None):
+ return dict.get(self, self._actual(key), default)
+
+ def pop(self, key, *default):
+ return dict.pop(self, self._actual(key), *default)
+
+ def __setitem__(self, key, value):
+ # запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
+ dict.__setitem__(self, self._actual(key), value)
+
+def ci_json(obj):
+ """Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
+ if isinstance(obj, dict):
+ return CIDict((k, ci_json(v)) for k, v in obj.items())
+ if isinstance(obj, list):
+ return [ci_json(v) for v in obj]
+ return obj
+
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
@@ -698,7 +735,7 @@ def main():
sys.exit(1)
with open(json_path, 'r', encoding='utf-8-sig') as f:
- defn = json.load(f)
+ defn = ci_json(json.load(f))
if not defn.get('name'):
print("JSON must have 'name' field (role programmatic name)", file=sys.stderr)
diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1
index 7cc4a7df..c731b3d4 100644
--- a/.claude/skills/skd-compile/scripts/skd-compile.ps1
+++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1
@@ -1,4 +1,4 @@
-# skd-compile v1.116 — Compile 1C DCS from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
+# skd-compile v1.117 — Compile 1C DCS from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py
index a6165a09..9b6b2875 100644
--- a/.claude/skills/skd-compile/scripts/skd-compile.py
+++ b/.claude/skills/skd-compile/scripts/skd-compile.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# skd-compile v1.116 — Compile 1C DCS from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
+# skd-compile v1.117 — Compile 1C DCS from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -12,6 +12,43 @@ from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
+class CIDict(dict):
+ # Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
+ # реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
+ # сохраняется — от него зависит порядок эмиссии.
+ def _actual(self, key):
+ if not isinstance(key, str) or dict.__contains__(self, key):
+ return key
+ ci = self.__dict__.get('_ci')
+ if ci is None or len(ci) != len(self):
+ ci = {k.lower(): k for k in self if isinstance(k, str)}
+ self.__dict__['_ci'] = ci
+ return ci.get(key.lower(), key)
+
+ def __getitem__(self, key):
+ return dict.__getitem__(self, self._actual(key))
+
+ def __contains__(self, key):
+ return dict.__contains__(self, self._actual(key))
+
+ def get(self, key, default=None):
+ return dict.get(self, self._actual(key), default)
+
+ def pop(self, key, *default):
+ return dict.pop(self, self._actual(key), *default)
+
+ def __setitem__(self, key, value):
+ # запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
+ dict.__setitem__(self, self._actual(key), value)
+
+def ci_json(obj):
+ """Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
+ if isinstance(obj, dict):
+ return CIDict((k, ci_json(v)) for k, v in obj.items())
+ if isinstance(obj, list):
+ return [ci_json(v) for v in obj]
+ return obj
+
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
@@ -1592,7 +1629,7 @@ def load_user_styles(base_dir, output_path=None):
for p in search_paths:
if os.path.isfile(p):
with open(p, 'r', encoding='utf-8-sig') as f:
- user_styles = json.load(f)
+ user_styles = ci_json(json.load(f))
for name, overrides in user_styles.items():
base = dict(AREA_STYLE_PRESETS.get(name, AREA_STYLE_PRESETS['data']))
base.update(overrides)
@@ -3029,7 +3066,7 @@ def main():
else:
json_text = args.Value
- defn = json.loads(json_text)
+ defn = ci_json(json.loads(json_text))
if not defn.get('dataSets') or len(defn['dataSets']) == 0:
print("JSON must have at least one entry in 'dataSets'", file=sys.stderr)
diff --git a/.claude/skills/subsystem-compile/scripts/subsystem-compile.ps1 b/.claude/skills/subsystem-compile/scripts/subsystem-compile.ps1
index a79a11b0..2b783c3c 100644
--- a/.claude/skills/subsystem-compile/scripts/subsystem-compile.ps1
+++ b/.claude/skills/subsystem-compile/scripts/subsystem-compile.ps1
@@ -1,4 +1,4 @@
-# subsystem-compile v1.24 — Create 1C subsystem from JSON definition (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
+# subsystem-compile v1.25 — Create 1C subsystem from JSON definition (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
diff --git a/.claude/skills/subsystem-compile/scripts/subsystem-compile.py b/.claude/skills/subsystem-compile/scripts/subsystem-compile.py
index 0db65347..97360510 100644
--- a/.claude/skills/subsystem-compile/scripts/subsystem-compile.py
+++ b/.claude/skills/subsystem-compile/scripts/subsystem-compile.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# subsystem-compile v1.24 — Create 1C subsystem from JSON definition (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
+# subsystem-compile v1.25 — Create 1C subsystem from JSON definition (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -13,6 +13,43 @@ from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
+class CIDict(dict):
+ # Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
+ # реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
+ # сохраняется — от него зависит порядок эмиссии.
+ def _actual(self, key):
+ if not isinstance(key, str) or dict.__contains__(self, key):
+ return key
+ ci = self.__dict__.get('_ci')
+ if ci is None or len(ci) != len(self):
+ ci = {k.lower(): k for k in self if isinstance(k, str)}
+ self.__dict__['_ci'] = ci
+ return ci.get(key.lower(), key)
+
+ def __getitem__(self, key):
+ return dict.__getitem__(self, self._actual(key))
+
+ def __contains__(self, key):
+ return dict.__contains__(self, self._actual(key))
+
+ def get(self, key, default=None):
+ return dict.get(self, self._actual(key), default)
+
+ def pop(self, key, *default):
+ return dict.pop(self, self._actual(key), *default)
+
+ def __setitem__(self, key, value):
+ # запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
+ dict.__setitem__(self, self._actual(key), value)
+
+def ci_json(obj):
+ """Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
+ if isinstance(obj, dict):
+ return CIDict((k, ci_json(v)) for k, v in obj.items())
+ if isinstance(obj, list):
+ return [ci_json(v) for v in obj]
+ return obj
+
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
@@ -382,7 +419,7 @@ def main():
else:
json_text = args.Value
- defn = json.loads(json_text)
+ defn = ci_json(json.loads(json_text))
if not defn.get('name'):
print("JSON must have 'name' field", file=sys.stderr)
diff --git a/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1 b/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1
index ea29e995..adb1e86e 100644
--- a/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1
+++ b/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1
@@ -1,4 +1,4 @@
-# subsystem-edit v1.19 — Edit existing 1C subsystem XML (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
+# subsystem-edit v1.20 — Edit existing 1C subsystem XML (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][Alias('Path')][string]$SubsystemPath,
diff --git a/.claude/skills/subsystem-edit/scripts/subsystem-edit.py b/.claude/skills/subsystem-edit/scripts/subsystem-edit.py
index 956a5852..b578b030 100644
--- a/.claude/skills/subsystem-edit/scripts/subsystem-edit.py
+++ b/.claude/skills/subsystem-edit/scripts/subsystem-edit.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# subsystem-edit v1.19 — Edit existing 1C subsystem XML (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
+# subsystem-edit v1.20 — Edit existing 1C subsystem XML (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -13,6 +13,43 @@ from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
+class CIDict(dict):
+ # Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
+ # реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
+ # сохраняется — от него зависит порядок эмиссии.
+ def _actual(self, key):
+ if not isinstance(key, str) or dict.__contains__(self, key):
+ return key
+ ci = self.__dict__.get('_ci')
+ if ci is None or len(ci) != len(self):
+ ci = {k.lower(): k for k in self if isinstance(k, str)}
+ self.__dict__['_ci'] = ci
+ return ci.get(key.lower(), key)
+
+ def __getitem__(self, key):
+ return dict.__getitem__(self, self._actual(key))
+
+ def __contains__(self, key):
+ return dict.__contains__(self, self._actual(key))
+
+ def get(self, key, default=None):
+ return dict.get(self, self._actual(key), default)
+
+ def pop(self, key, *default):
+ return dict.pop(self, self._actual(key), *default)
+
+ def __setitem__(self, key, value):
+ # запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
+ dict.__setitem__(self, self._actual(key), value)
+
+def ci_json(obj):
+ """Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
+ if isinstance(obj, dict):
+ return CIDict((k, ci_json(v)) for k, v in obj.items())
+ if isinstance(obj, list):
+ return [ci_json(v) for v in obj]
+ return obj
+
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
@@ -482,7 +519,7 @@ def parse_value_list(val):
"""Parse a string or JSON array into a list of strings."""
val = val.strip()
if val.startswith("["):
- arr = json.loads(val)
+ arr = ci_json(json.loads(val))
return [str(item) for item in arr]
return [val]
@@ -740,7 +777,7 @@ def main():
def do_set_property(json_val):
nonlocal modify_count
- prop_def = json.loads(json_val)
+ prop_def = ci_json(json.loads(json_val))
prop_name = str(prop_def["name"])
prop_value = str(prop_def.get("value", ""))
@@ -837,7 +874,7 @@ def main():
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh:
- ops = json.loads(fh.read())
+ ops = ci_json(json.loads(fh.read()))
if isinstance(ops, list):
operations = ops
else:
diff --git a/tests/skills/cases/cf-edit/lenient-key-case.json b/tests/skills/cases/cf-edit/lenient-key-case.json
new file mode 100644
index 00000000..8d8f1ab4
--- /dev/null
+++ b/tests/skills/cases/cf-edit/lenient-key-case.json
@@ -0,0 +1,9 @@
+{
+ "name": "Прощающий ввод: регистр ключей операции",
+ "input": [
+ {
+ "Operation": "modify-property",
+ "Value": "Version=3.0.0"
+ }
+ ]
+}
diff --git a/tests/skills/cases/cf-edit/snapshots/lenient-key-case/Configuration.xml b/tests/skills/cases/cf-edit/snapshots/lenient-key-case/Configuration.xml
new file mode 100644
index 00000000..afff09d4
--- /dev/null
+++ b/tests/skills/cases/cf-edit/snapshots/lenient-key-case/Configuration.xml
@@ -0,0 +1,251 @@
+
+
Русский
+