From a6ca515c9612bcdcab02d16baf2fc5982a26189f Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 9 Jul 2026 12:26:00 +0300 Subject: [PATCH] =?UTF-8?q?fix(meta-compile):=20#38=20=E2=80=94=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B2=20Configuration.xml=20=D0=B1=D0=B5=D0=B7=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D0=B5=D1=80=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20(=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20xmlns)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Блок «Register in Configuration.xml» py-порта пересоздавал весь файл через ElementTree.write, который объявляет в корне только namespace из имён элементов/ атрибутов и молча выкидывает объявления, живущие лишь в значениях атрибутов (xsi:type="app:ApplicationUsePurpose" в UsePurposes). Необъявленный префикс → XDTO читает значение как anyType → Конфигуратор отказывается грузить Configuration.xml. Теперь ET используется только read-only (поиск ChildObjects + проверка дубля), а запись — текстовой вставкой в исходное содержимое с сохранением BOM/EOL/всех xmlns байт-в-байт (как уже делает subsystem-compile). Многострочная группировка по типу, обработка self-closing , идемпотентность сохранены. PS1-порт (XmlDocument хранит xmlns как атрибутные узлы) багу не подвержен — функциональных правок нет, версия синхронно поднята до v1.15. tests(runner): снят xmlns-strip нормализатора для Configuration.xml — он маскировал баг и прятал бы регрессии. Полный python-suite 472/472; на старом коде un-masked runner ловит поломку (30/33). Радиус изменения нулевой (прочие навыки не роняют xmlns). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../meta-compile/scripts/meta-compile.ps1 | 2 +- .../meta-compile/scripts/meta-compile.py | 88 ++++++++++--------- tests/skills/runner.mjs | 22 +++-- 3 files changed, 61 insertions(+), 51 deletions(-) diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index d4e5c0dd..d6e027ee 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.14 — Compile 1C metadata object from JSON +# meta-compile v1.15 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py index bdbabb9f..d7d7e3cd 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.14 — Compile 1C metadata object from JSON +# meta-compile v1.15 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -2772,21 +2772,19 @@ reg_result = None child_tag = obj_type if os.path.isfile(config_xml_path): - # Parse preserving whitespace via raw string manipulation - with open(config_xml_path, 'r', encoding='utf-8-sig') as f: + # Read raw content, preserving BOM/EOL byte-for-byte (newline='' => no translation). + with open(config_xml_path, 'r', encoding='utf-8-sig', newline='') as f: config_content = f.read() ns = 'http://v8.1c.ru/8.3/MDClasses' - ET.register_namespace('', ns) - # Parse all namespaces used in the file - # Use iterparse to collect namespace prefixes - namespaces_in_file = {} - for evt, elem in ET.iterparse(config_xml_path, events=['start-ns']): - prefix, uri = elem - if prefix: - namespaces_in_file[prefix] = uri - ET.register_namespace(prefix, uri) - + # ET is used ONLY read-only here: to locate ChildObjects and detect a duplicate. + # We deliberately do NOT re-serialize Configuration.xml with ElementTree.write(): + # it drops every xmlns declaration used only inside attribute VALUES (e.g. + # xsi:type="app:ApplicationUsePurpose" in UsePurposes) because ET never sees those + # prefixes in element/attribute names. The dropped declaration makes XDTO read the + # value as anyType and Designer refuses to load the file (issue #38). Registration is + # therefore done by raw-text insertion, preserving BOM, EOL and all namespaces + # byte-for-byte (same approach as subsystem-compile). tree = ET.parse(config_xml_path) root = tree.getroot() @@ -2797,41 +2795,47 @@ if os.path.isfile(config_xml_path): if config_elem is not None: child_objects = config_elem.find(f'{{{ns}}}ChildObjects') - if child_objects is not None: + if child_objects is None: + reg_result = 'no-childobj' + else: existing = child_objects.findall(f'{{{ns}}}{child_tag}') - already_exists = False - for e in existing: - if (e.text or '').strip() == obj_name: - already_exists = True - break + already_exists = any((e.text or '').strip() == obj_name for e in existing) if already_exists: reg_result = 'already' else: - new_elem = ET.SubElement(child_objects, f'{{{ns}}}{child_tag}') - new_elem.text = obj_name + eol = '\r\n' if '\r\n' in config_content else '\n' + entry = f'<{child_tag}>{esc_xml(obj_name)}' - if existing: - # Insert after last existing element of same type - last_elem = existing[-1] - all_children = list(child_objects) - idx = all_children.index(last_elem) - child_objects.remove(new_elem) - child_objects.insert(idx + 1, new_elem) - - # Write back preserving BOM - tree.write(config_xml_path, encoding='utf-8', xml_declaration=True) - # Re-read to add BOM, fix declaration quotes, ensure trailing newline - with open(config_xml_path, 'r', encoding='utf-8') as f: - raw = f.read() - if raw.startswith(""): - raw = raw.replace("", '', 1) - if not raw.endswith('\n'): - raw += '\n' - write_utf8_bom(config_xml_path, raw) - reg_result = 'added' - else: - reg_result = 'no-childobj' + block = re.search(r'.*?', config_content, re.S) + if block is None: + # Empty self-closing => open it with the first entry. + empty = re.search(r'', config_content) + if empty is None: + reg_result = 'no-childobj' + else: + replacement = f'{eol}\t\t\t{entry}{eol}\t\t' + new_content = config_content[:empty.start()] + replacement + config_content[empty.end():] + write_utf8_bom(config_xml_path, new_content) + reg_result = 'added' + else: + close_same = f'' + last_same = config_content.rfind(close_same, block.start(), block.end()) + if last_same != -1: + # After the last element of the same type (keeps them grouped). + insert_at = last_same + len(close_same) + new_content = (config_content[:insert_at] + + f'{eol}\t\t\t{entry}' + + config_content[insert_at:]) + else: + # No element of this type yet: new line before , + # reusing the block's existing closing indent for . + close_at = config_content.rfind('', block.start(), block.end()) + new_content = (config_content[:close_at] + + f'\t{entry}{eol}\t\t' + + config_content[close_at:]) + write_utf8_bom(config_xml_path, new_content) + reg_result = 'added' else: reg_result = 'no-config' diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs index ec9790c0..8a1dec65 100644 --- a/tests/skills/runner.mjs +++ b/tests/skills/runner.mjs @@ -292,7 +292,7 @@ function buildArgs(skillConfig, caseData, workDir, inputFilePath, runtime) { const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi; -function normalizeXmlContent(text) { +function normalizeXmlContent(text, opts = {}) { let s = text; // 1. XML declaration: normalize quotes and encoding case s = s.replace( @@ -301,8 +301,13 @@ function normalizeXmlContent(text) { ); // 2. Remove (CR encoded as XML entity by Python etree) s = s.replace(/ /g, ''); - // 3. Strip xmlns declarations (Python etree strips unused ones) - s = s.replace(/\s+xmlns(?::[\w]+)?="[^"]*"/g, ''); + // 3. Strip xmlns declarations (Python etree strips unused ones). + // Skipped for Configuration.xml: those declarations are load-bearing (they back + // xsi:type values like app:ApplicationUsePurpose in UsePurposes) and dropping them + // is exactly the corruption of issue #38 — keeping them lets the test guard against it. + if (!opts.keepXmlns) { + s = s.replace(/\s+xmlns(?::[\w]+)?="[^"]*"/g, ''); + } // 4. Normalize self-closing tags: remove space before /> s = s.replace(/\s*\/>/g, '/>'); // 5. Collapse whitespace between tags: "> \n\t <" → "><" @@ -314,14 +319,15 @@ function normalizeXmlContent(text) { return s; } -function normalizeContent(text, config) { +function normalizeContent(text, config, relFile) { // Strip BOM let s = text.replace(/^\uFEFF/, ''); // Normalize line endings s = s.replace(/\r\n/g, '\n'); // Normalize XML differences (Python etree serialization quirks) if (config?.runtime === 'python') { - s = normalizeXmlContent(s); + const base = relFile ? relFile.split(/[\\/]/).pop() : ''; + s = normalizeXmlContent(s, { keepXmlns: base === 'Configuration.xml' }); } // Normalize UUIDs @@ -403,8 +409,8 @@ function compareSnapshot(workDir, snapshotDir, snapshotConfig) { const actualRaw = readFileSync(actualPath, 'utf8'); const snapshotRaw = readFileSync(snapshotPath, 'utf8'); - const actual = normalizeContent(actualRaw, snapshotConfig); - const expected = normalizeContent(snapshotRaw, snapshotConfig); + const actual = normalizeContent(actualRaw, snapshotConfig, relFile); + const expected = normalizeContent(snapshotRaw, snapshotConfig, relFile); if (actual !== expected) { // Find first differing line @@ -446,7 +452,7 @@ function updateSnapshot(workDir, snapshotDir, snapshotConfig) { mkdirSync(dirname(dst), { recursive: true }); const raw = readFileSync(src, 'utf8'); - const normalized = normalizeContent(raw, snapshotConfig); + const normalized = normalizeContent(raw, snapshotConfig, relFile); writeFileSync(dst, normalized, 'utf8'); } }