fix(skills): регистронезависимые ключи DSL в py-портах

Дорожка DSL из кампании паритета. PowerShell читает свойства объекта из
ConvertFrom-Json без учёта регистра, Python — точным совпадением, поэтому
"Name" вместо "name" в py-порте молча не находился: навык печатал [OK], а
свойство в выход не попадало. На skd-compile это давало схему без запроса.

CIDict + ci_json (эталон — meta-compile) внесены в 10 портов, читающих
пользовательский DSL: cf-edit, form-compile, form-edit, interface-edit,
meta-edit, mxl-compile, role-compile, skd-compile, subsystem-compile,
subsystem-edit. Обёрнуты точки разбора DSL, операций и JSON, приходящего
значением параметра; реестр .v8-project.json намеренно не трогаем.

skd-edit в список не входит: он принимает операцию через -Operation с
choices, что уже закрыто ci_parse_args.

Каждому навыку добавлен кейс lenient-key-case: вход с перемешанным регистром
ключей, эталон один на оба порта — раннер гоняет обе среды, поэтому кейс сам
по себе проверяет паритет.

Проверка: 683/683 (PS), 680+3 skipped (PY), гарды 4/4, версии портов совпадают.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-09 20:15:39 +03:00
co-authored by Claude Opus 5
parent d325a3d5af
commit 57b0f95b5f
80 changed files with 3500 additions and 44 deletions
@@ -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: