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