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'); } }