diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index 142761a4..0d933c1e 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.93 — Compile 1C metadata object from JSON +# meta-compile v1.94 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -408,6 +408,11 @@ $validTypes = @("Catalog","Document","Enum","Constant","InformationRegister","Ac "Sequence","FilterCriterion","DocumentNumerator","SettingsStorage","CommonForm", "SessionParameter","CommonCommand","CommandGroup","CommonAttribute","FunctionalOptionsParameter","WSReference", "CommonPicture","CommonTemplate") +# -notin регистронезависим, поэтому "catalog" проходил проверку и дальше шёл в ИМЯ ТЕГА и в +# Configuration.xml как есть — выгрузка получалась с , которую платформа не принимает. +# Прощаем регистр, но приводим к канону списка. +$canonType = $validTypes | Where-Object { $_ -eq $objType } | Select-Object -First 1 +if ($canonType) { $objType = $canonType } if ($objType -notin $validTypes) { Write-Error "Unsupported type: $objType. Valid: $($validTypes -join ', ')" exit 1 diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py index a4004250..c30c018c 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.93 — Compile 1C metadata object from JSON +# meta-compile v1.94 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -16,6 +16,69 @@ from lxml import etree sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") +# ============================================================ +# Регистронезависимый ввод — паритет с PS1. В PowerShell регистр не значим нигде, куда +# смотрит пользовательский ввод: свойства объекта из ConvertFrom-Json, ключи Hashtable, +# -eq/-contains, имена параметров, ValidateSet. В Python совпадение точное, поэтому порт +# молча терял свойства DSL, написанные в другом регистре. Обёртки ниже выравнивают поведение. +# ============================================================ + +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) + names = {s.lower(): s for a in parser._actions for s in a.option_strings} + for i, tok in enumerate(argv): + if tok.startswith('-') and tok.lower() in names: + argv[i] = names[tok.lower()] + # choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр + choice_map = {} + for a in parser._actions: + if a.choices: + for s in a.option_strings: + choice_map[s] = {str(c).lower(): c for c in a.choices} + for i in range(len(argv) - 1): + m = choice_map.get(argv[i]) + if m and argv[i + 1].lower() in m: + argv[i + 1] = m[argv[i + 1].lower()] + return parser.parse_args(argv) + # ============================================================ # Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md # Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin @@ -302,7 +365,7 @@ def split_camel_case(name): parser = argparse.ArgumentParser(allow_abbrev=False) parser.add_argument('-JsonPath', required=True) parser.add_argument('-OutputDir', required=True) -args = parser.parse_args() +args = ci_parse_args(parser) json_path = args.JsonPath output_dir = args.OutputDir @@ -314,7 +377,7 @@ if not os.path.isfile(json_path): with open(json_path, 'r', encoding='utf-8-sig') as f: json_text = f.read() -defn = json.loads(json_text) +defn = ci_json(json.loads(json_text)) assert_edit_allowed(output_dir, "editable") @@ -408,6 +471,10 @@ enum_value_aliases = { 'ИндексироватьСДопУпорядочиванием': 'IndexWithAdditionalOrder', } +# Словари, по которым ищут ПОЛЬЗОВАТЕЛЬСКИЙ ввод, — регистронезависимы, как хеш-таблицы PS1. +object_type_synonyms = CIDict(object_type_synonyms) +enum_value_aliases = CIDict(enum_value_aliases) + # Valid enum values per property (from meta-validate) valid_enum_values = { 'RegisterType': ['Balance', 'Turnovers'], @@ -566,6 +633,8 @@ valid_types = [ 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', ] +# Регистр имени вида — как в PS (-contains регистронезависим): приводим к канону списка +obj_type = next((t for t in valid_types if t.lower() == obj_type.lower()), obj_type) if obj_type not in valid_types: print(f"Unsupported type: {obj_type}. Valid: {', '.join(valid_types)}", file=sys.stderr) sys.exit(1) @@ -652,6 +721,7 @@ cfg_object_kinds = {"Catalog", "Document", "Enum", "ChartOfAccounts", "ChartOfCh # Алиас на локальный словарь: тело resolve_type_str ниже — общая реализация, # одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs). +type_synonyms = CIDict(type_synonyms) TYPE_SYNONYMS = type_synonyms diff --git a/docs/python-porting-guide.md b/docs/python-porting-guide.md index 8269b7e7..9c5c643f 100644 --- a/docs/python-porting-guide.md +++ b/docs/python-porting-guide.md @@ -202,6 +202,31 @@ PS1 `@{}` (Hashtable) итерирует ключи в порядке хэш-к PS1: `'^(?i)desc$'` — работает. Python 3.11+: `r'^(?i)desc$'` — ошибка. Inline-флаг `(?i)` должен быть в начале строки паттерна: `r'(?i)^desc$'` или `re.IGNORECASE`. +### Регистр: PS не различает его нигде, Python различает везде + +Самая дорогая ловушка портирования, потому что молчаливая. В PowerShell регистр не значим во +всём, куда попадает пользовательский ввод: свойства объекта из `ConvertFrom-Json`, ключи +`Hashtable`, `-eq`/`-contains`, имена параметров, `[ValidateSet]`. В Python совпадение точное, +поэтому `"CodeLength"` вместо `"codeLength"` в py-порте просто не находится: навык печатает +`[OK]`, а свойство в выход не попадает. + +В py-портах ввод поэтому проходит через общие обёртки (реестр — в +`tests/skills/check-inline-drift.mjs`, семьи `case-insensitive input: *`): + +```python +args = ci_parse_args(parser) # имена параметров и значения choices — как ValidateSet +defn = ci_json(json.loads(json_text)) # словари DSL → CIDict: поиск без учёта регистра +enum_value_aliases = CIDict(enum_value_aliases) # словари, по которым ищут ввод пользователя +``` + +`CIDict` хранит ключи КАК ЕСТЬ — часть из них это имена объектов, попадающие в XML; +регистронезависим только поиск. В `.ps1` этих обёрток нет и быть не должно. + +Обёртка чинит поиск по словарю, но не сравнение строк: `if k in valid_types` в PS +регистронезависим (`-contains`), в Python нет. Такие места правятся точечно приведением к +одному регистру — и обязаны приводить значение к КАНОНУ списка, иначе прощённый регистр утечёт +в имя тега (в `meta-compile` `"type":"catalog"` так и делал ``). + ### Обращение к отсутствующим свойствам PS1 молча возвращает `$null` при обращении к несуществующему свойству (`.empty` на массиве). Python падает с `AttributeError`. Добавляйте `isinstance()` проверки при портировании. diff --git a/tests/skills/cases/meta-compile/lenient-key-case.json b/tests/skills/cases/meta-compile/lenient-key-case.json new file mode 100644 index 00000000..42947154 --- /dev/null +++ b/tests/skills/cases/meta-compile/lenient-key-case.json @@ -0,0 +1,31 @@ +{ + "name": "Прощающий ввод: регистр ключей DSL, вида объекта и значений enum", + "input": [ + { + "type": "Catalog", + "name": "Контрагенты", + "CodeLength": 15, + "DescriptionLength": 75, + "Hierarchical": true + }, + { + "type": "catalog", + "name": "Валюты", + "codeLength": 12 + }, + { + "type": "AccumulationRegister", + "name": "ПродажиОбороты", + "registerType": "обороты", + "dimensions": ["Номенклатура: CatalogRef.Номенклатура"], + "resources": ["Сумма: Number(15,2)"] + } + ], + "expect": { + "files": [ + "Catalogs/Контрагенты.xml", + "Catalogs/Валюты.xml", + "AccumulationRegisters/ПродажиОбороты.xml" + ] + } +} diff --git a/tests/skills/cases/meta-compile/snapshots/lenient-key-case/AccumulationRegisters/ПродажиОбороты.xml b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/AccumulationRegisters/ПродажиОбороты.xml new file mode 100644 index 00000000..e21521e9 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/AccumulationRegisters/ПродажиОбороты.xml @@ -0,0 +1,238 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + UUID-012 + UUID-013 + + + + ПродажиОбороты + + + ru + Продажи обороты + + + + true + + + Turnovers + false + + + + DontCheck + false + false + Auto + + + false + + + Auto + Auto + + false + Use + false + + + + Use + + + + + + + + DontCheck + false + false + Auto + + + false + + + Auto + Auto + + false + Use + false + + + + Use + + + + + + + + DontCheck + false + false + Auto + + + false + + + Auto + Auto + + false + Use + false + + + + Use + + + + + + + + DontCheck + false + false + Auto + + + false + + + Auto + Auto + + false + Use + false + + + + Use + + + + + + + Managed + Use + true + + + + + + + + Сумма + + + ru + Сумма + + + + + xs:decimal + + 15 + 2 + Any + + + false + + + + false + + false + false + + + DontCheck + Items + + + Auto + Auto + + + Auto + Use + + + + + Номенклатура + + + ru + Номенклатура + + + + + cfg:CatalogRef.Номенклатура + + false + + + + false + + false + false + + + DontCheck + Items + + + Auto + Auto + + + Auto + false + DontIndex + Use + true + + + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/lenient-key-case/AccumulationRegisters/ПродажиОбороты/Ext/RecordSetModule.bsl b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/AccumulationRegisters/ПродажиОбороты/Ext/RecordSetModule.bsl new file mode 100644 index 00000000..e69de29b diff --git a/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Catalogs/Валюты.xml b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Catalogs/Валюты.xml new file mode 100644 index 00000000..db145771 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Catalogs/Валюты.xml @@ -0,0 +1,91 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + + Валюты + + + ru + Валюты + + + + false + HierarchyFoldersAndItems + false + 2 + true + true + + ToItems + 12 + 25 + String + Variable + WholeCatalog + false + true + AsDescription + + Auto + InDialog + false + BothWays + + Catalog.Валюты.StandardAttribute.Description + Catalog.Валюты.StandardAttribute.Code + + Begin + DontUse + Directly + + + + + + + + + + + false + + + Managed + Use + + + + + + Use + Auto + DontUse + false + false + + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Catalogs/Валюты/Ext/ObjectModule.bsl b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Catalogs/Валюты/Ext/ObjectModule.bsl new file mode 100644 index 00000000..e69de29b diff --git a/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Catalogs/Контрагенты.xml b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Catalogs/Контрагенты.xml new file mode 100644 index 00000000..77af5207 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Catalogs/Контрагенты.xml @@ -0,0 +1,91 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + + Контрагенты + + + ru + Контрагенты + + + + true + HierarchyFoldersAndItems + false + 2 + true + true + + ToItems + 15 + 75 + String + Variable + WholeCatalog + false + true + AsDescription + + Auto + InDialog + false + BothWays + + Catalog.Контрагенты.StandardAttribute.Description + Catalog.Контрагенты.StandardAttribute.Code + + Begin + DontUse + Directly + + + + + + + + + + + false + + + Managed + Use + + + + + + Use + Auto + DontUse + false + false + + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Catalogs/Контрагенты/Ext/ObjectModule.bsl b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Catalogs/Контрагенты/Ext/ObjectModule.bsl new file mode 100644 index 00000000..e69de29b diff --git a/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Configuration.xml b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Configuration.xml new file mode 100644 index 00000000..c1906941 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Configuration.xml @@ -0,0 +1,254 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + UUID-012 + UUID-013 + + + UUID-014 + UUID-015 + + + + TestConfig + + + ru + TestConfig + + + + + Version8_3_24 + ManagedApplication + + PlatformApplication + + Russian + + + + + false + false + false + + + + + + + + + + + + + + + + + + + + + + Biometrics + true + + + Location + false + + + BackgroundLocation + false + + + BluetoothPrinters + false + + + WiFiPrinters + false + + + Contacts + false + + + Calendars + false + + + PushNotifications + false + + + LocalNotifications + false + + + InAppPurchases + false + + + PersonalComputerFileExchange + false + + + Ads + false + + + NumberDialing + false + + + CallProcessing + false + + + CallLog + false + + + AutoSendSMS + false + + + ReceiveSMS + false + + + SMSLog + false + + + Camera + false + + + Microphone + false + + + MusicLibrary + false + + + PictureAndVideoLibraries + false + + + AudioPlaybackAndVibration + false + + + BackgroundAudioPlaybackAndVibration + false + + + InstallPackages + false + + + OSBackup + true + + + ApplicationUsageStatistics + false + + + BarcodeScanning + false + + + BackgroundAudioRecording + false + + + AllFilesAccess + false + + + Videoconferences + false + + + NFC + false + + + DocumentScanning + false + + + SpeechToText + false + + + Geofences + false + + + IncomingShareRequests + false + + + AllIncomingShareRequestsTypesProcessing + false + + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + TaxiEnableVersion8_2 + DontUse + Version8_3_24 + + + + Русский + Контрагенты + Валюты + ПродажиОбороты + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Ext/ClientApplicationInterface.xml b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Ext/ClientApplicationInterface.xml new file mode 100644 index 00000000..3c1161b2 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Ext/ClientApplicationInterface.xml @@ -0,0 +1,18 @@ + + + + + UUID-002 + + + + + UUID-004 + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Languages/Русский.xml b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Languages/Русский.xml new file mode 100644 index 00000000..37c60d78 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/lenient-key-case/Languages/Русский.xml @@ -0,0 +1,16 @@ + + + + + Русский + + + ru + Русский + + + + ru + + + \ No newline at end of file diff --git a/tests/skills/check-inline-drift.mjs b/tests/skills/check-inline-drift.mjs index cf5b7c35..27d6d5da 100644 --- a/tests/skills/check-inline-drift.mjs +++ b/tests/skills/check-inline-drift.mjs @@ -270,6 +270,23 @@ const FAMILIES = [ ], }, + // ─── Регистронезависимый ввод: паритет с PS1 ───────────────────────────── + // Существует только в PY: PowerShell регистронезависим сам по себе (свойства PSObject, ключи + // Hashtable, -eq/-contains, имена параметров, ValidateSet), поэтому в .ps1 копии нет и быть + // не должно — ps1: null. + { + name: 'case-insensitive input: CIDict', py: 'CIDict', ps1: null, + variants: [{ id: 'base', authority: 'meta-compile', consumers: [] }], + }, + { + name: 'case-insensitive input: ci_json', py: 'ci_json', ps1: null, + variants: [{ id: 'base', authority: 'meta-compile', consumers: [] }], + }, + { + name: 'case-insensitive input: ci_parse_args', py: 'ci_parse_args', ps1: null, + variants: [{ id: 'base', authority: 'meta-compile', consumers: [] }], + }, + ]; // ─── Семьи, разъехавшиеся целиком ─────────────────────────────────────────── @@ -301,7 +318,9 @@ function extractPy(text) { for (let i = 0; i < lines.length; i++) { // Определение бывает вложенным: *-info объявляют is_external_root внутри другой функции. // Поиск только по `^def` делал такие копии невидимыми для гарда — то есть давал ложное «OK». - const m = /^(\s*)def ([A-Za-z_]\w*)\(/.exec(lines[i]); + // Классы забирает та же ветка: общая утилита бывает и классом (CIDict), а без этого её тело + // гарду невидимо и семью для неё не завести. + const m = /^(\s*)(?:def|class) ([A-Za-z_]\w*)[(:]/.exec(lines[i]); if (!m) continue; const indent = m[1].length; const body = [lines[i]];